aboutsummaryrefslogtreecommitdiffstats
path: root/test/ng
diff options
context:
space:
mode:
authorMisko Hevery2012-03-23 14:03:24 -0700
committerMisko Hevery2012-03-28 11:16:35 -0700
commit2430f52bb97fa9d682e5f028c977c5bf94c5ec38 (patch)
treee7529b741d70199f36d52090b430510bad07f233 /test/ng
parent944098a4e0f753f06b40c73ca3e79991cec6c2e2 (diff)
downloadangular.js-2430f52bb97fa9d682e5f028c977c5bf94c5ec38.tar.bz2
chore(module): move files around in preparation for more modules
Diffstat (limited to 'test/ng')
-rw-r--r--test/ng/anchorScrollSpec.js186
-rw-r--r--test/ng/browserSpecs.js578
-rw-r--r--test/ng/cacheFactorySpec.js317
-rw-r--r--test/ng/compilerSpec.js1811
-rw-r--r--test/ng/controllerSpec.js73
-rw-r--r--test/ng/cookieStoreSpec.js30
-rw-r--r--test/ng/cookiesSpec.js97
-rw-r--r--test/ng/deferSpec.js113
-rw-r--r--test/ng/directive/aSpec.js46
-rw-r--r--test/ng/directive/booleanAttrDirSpecs.js125
-rw-r--r--test/ng/directive/formSpec.js294
-rw-r--r--test/ng/directive/inputSpec.js1119
-rw-r--r--test/ng/directive/ngBindSpec.js80
-rw-r--r--test/ng/directive/ngClassSpec.js204
-rw-r--r--test/ng/directive/ngClickSpec.js26
-rw-r--r--test/ng/directive/ngCloakSpec.js49
-rw-r--r--test/ng/directive/ngControllerSpec.js65
-rw-r--r--test/ng/directive/ngEventDirsSpec.js25
-rw-r--r--test/ng/directive/ngIncludeSpec.js289
-rw-r--r--test/ng/directive/ngInitSpec.js16
-rw-r--r--test/ng/directive/ngNonBindableSpec.js21
-rw-r--r--test/ng/directive/ngPluralizeSpec.js136
-rw-r--r--test/ng/directive/ngRepeatSpec.js289
-rw-r--r--test/ng/directive/ngShowHideSpec.js43
-rw-r--r--test/ng/directive/ngStyleSpec.js88
-rw-r--r--test/ng/directive/ngSwitchSpec.js93
-rw-r--r--test/ng/directive/ngViewSpec.js459
-rw-r--r--test/ng/directive/scriptSpec.js44
-rw-r--r--test/ng/directive/selectSpec.js863
-rw-r--r--test/ng/directive/styleSpec.js31
-rw-r--r--test/ng/documentSpec.js9
-rw-r--r--test/ng/exceptionHandlerSpec.js24
-rw-r--r--test/ng/filter/filterSpec.js69
-rw-r--r--test/ng/filter/filtersSpec.js280
-rw-r--r--test/ng/filter/limitToSpec.js52
-rw-r--r--test/ng/filter/orderBySpec.js34
-rw-r--r--test/ng/httpBackendSpec.js241
-rw-r--r--test/ng/httpSpec.js946
-rw-r--r--test/ng/interpolateSpec.js113
-rw-r--r--test/ng/localeSpec.js47
-rw-r--r--test/ng/locationSpec.js883
-rw-r--r--test/ng/logSpec.js121
-rw-r--r--test/ng/parseSpec.js631
-rw-r--r--test/ng/qSpec.js831
-rw-r--r--test/ng/resourceSpec.js325
-rw-r--r--test/ng/rootScopeSpec.js838
-rw-r--r--test/ng/routeParamsSpec.js20
-rw-r--r--test/ng/routeSpec.js471
-rw-r--r--test/ng/sanitizeSpec.js281
-rw-r--r--test/ng/snifferSpec.js33
-rw-r--r--test/ng/windowSpec.js7
51 files changed, 13866 insertions, 0 deletions
diff --git a/test/ng/anchorScrollSpec.js b/test/ng/anchorScrollSpec.js
new file mode 100644
index 00000000..7e4b3aa3
--- /dev/null
+++ b/test/ng/anchorScrollSpec.js
@@ -0,0 +1,186 @@
+describe('$anchorScroll', function() {
+
+ var elmSpy;
+
+ function addElements() {
+ var elements = sliceArgs(arguments);
+
+ return function() {
+ forEach(elements, function(identifier) {
+ var match = identifier.match(/(\w* )?(\w*)=(\w*)/),
+ jqElm = jqLite('<' + (match[1] || 'a ') + match[2] + '="' + match[3] + '"/>'),
+ elm = jqElm[0];
+
+ elmSpy[identifier] = spyOn(elm, 'scrollIntoView');
+ jqLite(document.body).append(jqElm);
+ });
+ };
+ }
+
+ function changeHashAndScroll(hash) {
+ return function($location, $anchorScroll) {
+ $location.hash(hash);
+ $anchorScroll();
+ };
+ }
+
+ function expectScrollingToTop($window) {
+ forEach(elmSpy, function(spy, id) {
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ expect($window.scrollTo).toHaveBeenCalledWith(0, 0);
+ }
+
+ function expectScrollingTo(identifier) {
+ return function($window) {
+ forEach(elmSpy, function(spy, id) {
+ if (identifier === id) expect(spy).toHaveBeenCalledOnce();
+ else expect(spy).not.toHaveBeenCalled();
+ });
+ expect($window.scrollTo).not.toHaveBeenCalled();
+ };
+ }
+
+ function expectNoScrolling() {
+ return expectScrollingTo(NaN);
+ }
+
+
+ beforeEach(module(function($provide) {
+ elmSpy = {};
+ $provide.value('$window', {
+ scrollTo: jasmine.createSpy('$window.scrollTo'),
+ document: document
+ });
+ }));
+
+
+ it('should scroll to top of the window if empty hash', inject(
+ changeHashAndScroll(''),
+ expectScrollingToTop));
+
+
+ it('should not scroll if hash does not match any element', inject(
+ addElements('id=one', 'id=two'),
+ changeHashAndScroll('non-existing'),
+ expectNoScrolling()));
+
+
+ it('should scroll to anchor element with name', inject(
+ addElements('a name=abc'),
+ changeHashAndScroll('abc'),
+ expectScrollingTo('a name=abc')));
+
+
+ it('should not scroll to other than anchor element with name', inject(
+ addElements('input name=xxl', 'select name=xxl', 'form name=xxl'),
+ changeHashAndScroll('xxl'),
+ expectNoScrolling()));
+
+
+ it('should scroll to anchor even if other element with given name exist', inject(
+ addElements('input name=some', 'a name=some'),
+ changeHashAndScroll('some'),
+ expectScrollingTo('a name=some')));
+
+
+ it('should scroll to element with id with precedence over name', inject(
+ addElements('name=abc', 'id=abc'),
+ changeHashAndScroll('abc'),
+ expectScrollingTo('id=abc')));
+
+
+ it('should scroll to top if hash == "top" and no matching element', inject(
+ changeHashAndScroll('top'),
+ expectScrollingToTop));
+
+
+ it('should scroll to element with id "top" if present', inject(
+ addElements('id=top'),
+ changeHashAndScroll('top'),
+ expectScrollingTo('id=top')));
+
+
+ describe('watcher', function() {
+
+ function initLocation(config) {
+ return function($provide, $locationProvider) {
+ $provide.value('$sniffer', {history: config.historyApi});
+ $locationProvider.html5Mode(config.html5Mode);
+ };
+ }
+
+ function changeHashTo(hash) {
+ return function ($location, $rootScope, $anchorScroll) {
+ $rootScope.$apply(function() {
+ $location.hash(hash);
+ });
+ };
+ }
+
+ function disableAutoScrolling() {
+ return function($anchorScrollProvider) {
+ $anchorScrollProvider.disableAutoScrolling();
+ };
+ }
+
+ afterEach(inject(function($document) {
+ dealoc($document);
+ }));
+
+
+ it('should scroll to element when hash change in hashbang mode', function() {
+ module(initLocation({html5Mode: false, historyApi: true}));
+ inject(
+ addElements('id=some'),
+ changeHashTo('some'),
+ expectScrollingTo('id=some')
+ );
+ });
+
+
+ it('should scroll to element when hash change in html5 mode with no history api', function() {
+ module(initLocation({html5Mode: true, historyApi: false}));
+ inject(
+ addElements('id=some'),
+ changeHashTo('some'),
+ expectScrollingTo('id=some')
+ );
+ });
+
+
+ it('should not scroll when element does not exist', function() {
+ module(initLocation({html5Mode: false, historyApi: false}));
+ inject(
+ addElements('id=some'),
+ changeHashTo('other'),
+ expectNoScrolling()
+ );
+ });
+
+
+ it('should scroll when html5 mode with history api', function() {
+ module(initLocation({html5Mode: true, historyApi: true}));
+ inject(
+ addElements('id=some'),
+ changeHashTo('some'),
+ expectScrollingTo('id=some')
+ );
+ });
+
+
+ it('should not scroll when disabled', function() {
+ module(
+ disableAutoScrolling(),
+ initLocation({html5Mode: false, historyApi: false})
+ );
+ inject(
+ addElements('id=fake'),
+ changeHashTo('fake'),
+ expectNoScrolling()
+ );
+ });
+ });
+});
+
diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js
new file mode 100644
index 00000000..4563d14b
--- /dev/null
+++ b/test/ng/browserSpecs.js
@@ -0,0 +1,578 @@
+'use strict';
+
+function MockWindow() {
+ var events = {};
+ var timeouts = this.timeouts = [];
+
+ this.setTimeout = function(fn) {
+ return timeouts.push(fn) - 1;
+ };
+
+ this.clearTimeout = function(id) {
+ timeouts[id] = noop;
+ };
+
+ this.setTimeout.flush = function() {
+ var length = timeouts.length;
+ while (length-- > 0) timeouts.shift()();
+ };
+
+ this.addEventListener = function(name, listener) {
+ if (isUndefined(events[name])) events[name] = [];
+ events[name].push(listener);
+ };
+
+ this.attachEvent = function(name, listener) {
+ this.addEventListener(name.substr(2), listener);
+ };
+
+ this.removeEventListener = noop;
+ this.detachEvent = noop;
+
+ this.fire = function(name) {
+ forEach(events[name], function(fn) {
+ fn({type: name}); // type to make jQuery happy
+ });
+ };
+
+ this.location = {
+ href: 'http://server',
+ replace: noop
+ };
+
+ this.history = {
+ replaceState: noop,
+ pushState: noop
+ };
+}
+
+describe('browser', function() {
+
+ var browser, fakeWindow, logs, scripts, removedScripts, sniffer;
+
+ beforeEach(function() {
+ scripts = [];
+ removedScripts = [];
+ sniffer = {history: true, hashchange: true};
+ fakeWindow = new MockWindow();
+
+ var fakeBody = [{appendChild: function(node){scripts.push(node);},
+ removeChild: function(node){removedScripts.push(node);}}];
+
+ logs = {log:[], warn:[], info:[], error:[]};
+
+ var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
+ warn: function() { logs.warn.push(slice.call(arguments)); },
+ info: function() { logs.info.push(slice.call(arguments)); },
+ error: function() { logs.error.push(slice.call(arguments)); }};
+
+ browser = new Browser(fakeWindow, jqLite(window.document), fakeBody, fakeLog, sniffer);
+ });
+
+ it('should contain cookie cruncher', function() {
+ expect(browser.cookies).toBeDefined();
+ });
+
+ describe('outstading requests', function() {
+ it('should process callbacks immedietly with no outstanding requests', function() {
+ var callback = jasmine.createSpy('callback');
+ browser.notifyWhenNoOutstandingRequests(callback);
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+
+ describe('defer', function() {
+ it('should execute fn asynchroniously via setTimeout', function() {
+ var callback = jasmine.createSpy('deferred');
+
+ browser.defer(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ fakeWindow.setTimeout.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should update outstandingRequests counter', function() {
+ var callback = jasmine.createSpy('deferred');
+
+ browser.defer(callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ fakeWindow.setTimeout.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should return unique deferId', function() {
+ var deferId1 = browser.defer(noop),
+ deferId2 = browser.defer(noop);
+
+ expect(deferId1).toBeDefined();
+ expect(deferId2).toBeDefined();
+ expect(deferId1).not.toEqual(deferId2);
+ });
+
+
+ describe('cancel', function() {
+ it('should allow tasks to be canceled with returned deferId', function() {
+ var log = [],
+ deferId1 = browser.defer(function() { log.push('cancel me'); }),
+ deferId2 = browser.defer(function() { log.push('ok'); }),
+ deferId3 = browser.defer(function() { log.push('cancel me, now!'); });
+
+ expect(log).toEqual([]);
+ expect(browser.defer.cancel(deferId1)).toBe(true);
+ expect(browser.defer.cancel(deferId3)).toBe(true);
+ fakeWindow.setTimeout.flush();
+ expect(log).toEqual(['ok']);
+ expect(browser.defer.cancel(deferId2)).toBe(false);
+ });
+ });
+ });
+
+
+ describe('cookies', function() {
+
+ function deleteAllCookies() {
+ var cookies = document.cookie.split(";");
+
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = cookies[i];
+ var eqPos = cookie.indexOf("=");
+ var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ }
+ }
+
+ beforeEach(function() {
+ deleteAllCookies();
+ expect(document.cookie).toEqual('');
+ });
+
+
+ afterEach(function() {
+ deleteAllCookies();
+ expect(document.cookie).toEqual('');
+ });
+
+
+ describe('remove all via (null)', function() {
+
+ it('should do nothing when no cookies are set', function() {
+ browser.cookies(null);
+ expect(document.cookie).toEqual('');
+ expect(browser.cookies()).toEqual({});
+ });
+
+ });
+
+ describe('remove via cookies(cookieName, undefined)', function() {
+
+ it('should remove a cookie when it is present', function() {
+ document.cookie = 'foo=bar';
+
+ browser.cookies('foo', undefined);
+
+ expect(document.cookie).toEqual('');
+ expect(browser.cookies()).toEqual({});
+ });
+
+
+ it('should do nothing when an nonexisting cookie is being removed', function() {
+ browser.cookies('doesntexist', undefined);
+ expect(document.cookie).toEqual('');
+ expect(browser.cookies()).toEqual({});
+ });
+ });
+
+
+ describe('put via cookies(cookieName, string)', function() {
+
+ it('should create and store a cookie', function() {
+ browser.cookies('cookieName', 'cookie=Value');
+ expect(document.cookie).toMatch(/cookieName=cookie%3DValue;? ?/);
+ expect(browser.cookies()).toEqual({'cookieName':'cookie=Value'});
+ });
+
+
+ it('should overwrite an existing unsynced cookie', function() {
+ document.cookie = "cookie=new";
+
+ var oldVal = browser.cookies('cookie', 'newer');
+
+ expect(document.cookie).toEqual('cookie=newer');
+ expect(browser.cookies()).toEqual({'cookie':'newer'});
+ expect(oldVal).not.toBeDefined();
+ });
+
+ it('should escape both name and value', function() {
+ browser.cookies('cookie1=', 'val;ue');
+ browser.cookies('cookie2=bar;baz', 'val=ue');
+
+ var rawCookies = document.cookie.split("; "); //order is not guaranteed, so we need to parse
+ expect(rawCookies.length).toEqual(2);
+ expect(rawCookies).toContain('cookie1%3D=val%3Bue');
+ expect(rawCookies).toContain('cookie2%3Dbar%3Bbaz=val%3Due');
+ });
+
+ it('should log warnings when 4kb per cookie storage limit is reached', function() {
+ var i, longVal = '', cookieStr;
+
+ for(i=0; i<4091; i++) {
+ longVal += '+';
+ }
+
+ cookieStr = document.cookie;
+ browser.cookies('x', longVal); //total size 4093-4096, so it should go through
+ expect(document.cookie).not.toEqual(cookieStr);
+ expect(browser.cookies()['x']).toEqual(longVal);
+ expect(logs.warn).toEqual([]);
+
+ browser.cookies('x', longVal + 'xxxx'); //total size 4097-4099, a warning should be logged
+ expect(logs.warn).toEqual(
+ [[ "Cookie 'x' possibly not set or overflowed because it was too large (4097 > 4096 " +
+ "bytes)!" ]]);
+
+ //force browser to dropped a cookie and make sure that the cache is not out of sync
+ browser.cookies('x', 'shortVal');
+ expect(browser.cookies().x).toEqual('shortVal'); //needed to prime the cache
+ cookieStr = document.cookie;
+ browser.cookies('x', longVal + longVal + longVal); //should be too long for all browsers
+
+ if (document.cookie !== cookieStr) {
+ fail("browser didn't drop long cookie when it was expected. make the cookie in this " +
+ "test longer");
+ }
+
+ expect(browser.cookies().x).toEqual('shortVal');
+ });
+
+ it('should log warnings when 20 cookies per domain storage limit is reached', function() {
+ var i, str, cookieStr;
+
+ for (i=0; i<20; i++) {
+ str = '' + i;
+ browser.cookies(str, str);
+ }
+
+ i=0;
+ for (str in browser.cookies()) {
+ i++;
+ }
+ expect(i).toEqual(20);
+ expect(logs.warn).toEqual([]);
+ cookieStr = document.cookie;
+
+ browser.cookies('one', 'more');
+ expect(logs.warn).toEqual([]);
+
+ //if browser dropped a cookie (very likely), make sure that the cache is not out of sync
+ if (document.cookie === cookieStr) {
+ expect(size(browser.cookies())).toEqual(20);
+ } else {
+ expect(size(browser.cookies())).toEqual(21);
+ }
+ });
+ });
+
+
+ describe('get via cookies()[cookieName]', function() {
+
+ it('should return undefined for nonexistent cookie', function() {
+ expect(browser.cookies().nonexistent).not.toBeDefined();
+ });
+
+
+ it ('should return a value for an existing cookie', function() {
+ document.cookie = "foo=bar=baz";
+ expect(browser.cookies().foo).toEqual('bar=baz');
+ });
+
+
+ it ('should unescape cookie values that were escaped by puts', function() {
+ document.cookie = "cookie2%3Dbar%3Bbaz=val%3Due";
+ expect(browser.cookies()['cookie2=bar;baz']).toEqual('val=ue');
+ });
+
+
+ it('should preserve leading & trailing spaces in names and values', function() {
+ browser.cookies(' cookie name ', ' cookie value ');
+ expect(browser.cookies()[' cookie name ']).toEqual(' cookie value ');
+ expect(browser.cookies()['cookie name']).not.toBeDefined();
+ });
+ });
+
+
+ describe('getAll via cookies()', function() {
+
+ it('should return cookies as hash', function() {
+ document.cookie = "foo1=bar1";
+ document.cookie = "foo2=bar2";
+ expect(browser.cookies()).toEqual({'foo1':'bar1', 'foo2':'bar2'});
+ });
+
+
+ it('should return empty hash if no cookies exist', function() {
+ expect(browser.cookies()).toEqual({});
+ });
+ });
+
+
+ it('should pick up external changes made to browser cookies', function() {
+ browser.cookies('oatmealCookie', 'drool');
+ expect(browser.cookies()).toEqual({'oatmealCookie':'drool'});
+
+ document.cookie = 'oatmealCookie=changed';
+ expect(browser.cookies().oatmealCookie).toEqual('changed');
+ });
+
+
+ it('should initialize cookie cache with existing cookies', function() {
+ document.cookie = "existingCookie=existingValue";
+ expect(browser.cookies()).toEqual({'existingCookie':'existingValue'});
+ });
+
+ });
+
+ describe('poller', function() {
+
+ it('should call functions in pollFns in regular intervals', function() {
+ var log = '';
+ browser.addPollFn(function() {log+='a';});
+ browser.addPollFn(function() {log+='b';});
+ expect(log).toEqual('');
+ fakeWindow.setTimeout.flush();
+ expect(log).toEqual('ab');
+ fakeWindow.setTimeout.flush();
+ expect(log).toEqual('abab');
+ });
+
+ it('should startPoller', function() {
+ expect(fakeWindow.timeouts.length).toEqual(0);
+
+ browser.addPollFn(function() {});
+ expect(fakeWindow.timeouts.length).toEqual(1);
+
+ //should remain 1 as it is the check fn
+ browser.addPollFn(function() {});
+ expect(fakeWindow.timeouts.length).toEqual(1);
+ });
+
+ it('should return fn that was passed into addPollFn', function() {
+ var fn = function() { return 1; };
+ var returnedFn = browser.addPollFn(fn);
+ expect(returnedFn).toBe(fn);
+ });
+ });
+
+ describe('url', function() {
+ var pushState, replaceState, locationReplace;
+
+ beforeEach(function() {
+ pushState = spyOn(fakeWindow.history, 'pushState');
+ replaceState = spyOn(fakeWindow.history, 'replaceState');
+ locationReplace = spyOn(fakeWindow.location, 'replace');
+ });
+
+ it('should return current location.href', function() {
+ fakeWindow.location.href = 'http://test.com';
+ expect(browser.url()).toEqual('http://test.com');
+
+ fakeWindow.location.href = 'https://another.com';
+ expect(browser.url()).toEqual('https://another.com');
+ });
+
+ it('should use history.pushState when available', function() {
+ sniffer.history = true;
+ browser.url('http://new.org');
+
+ expect(pushState).toHaveBeenCalledOnce();
+ expect(pushState.argsForCall[0][2]).toEqual('http://new.org');
+
+ expect(replaceState).not.toHaveBeenCalled();
+ expect(locationReplace).not.toHaveBeenCalled();
+ expect(fakeWindow.location.href).toEqual('http://server');
+ });
+
+ it('should use history.replaceState when available', function() {
+ sniffer.history = true;
+ browser.url('http://new.org', true);
+
+ expect(replaceState).toHaveBeenCalledOnce();
+ expect(replaceState.argsForCall[0][2]).toEqual('http://new.org');
+
+ expect(pushState).not.toHaveBeenCalled();
+ expect(locationReplace).not.toHaveBeenCalled();
+ expect(fakeWindow.location.href).toEqual('http://server');
+ });
+
+ it('should set location.href when pushState not available', function() {
+ sniffer.history = false;
+ browser.url('http://new.org');
+
+ expect(fakeWindow.location.href).toEqual('http://new.org');
+
+ expect(pushState).not.toHaveBeenCalled();
+ expect(replaceState).not.toHaveBeenCalled();
+ expect(locationReplace).not.toHaveBeenCalled();
+ });
+
+ it('should use location.replace when history.replaceState not available', function() {
+ sniffer.history = false;
+ browser.url('http://new.org', true);
+
+ expect(locationReplace).toHaveBeenCalledWith('http://new.org');
+
+ expect(pushState).not.toHaveBeenCalled();
+ expect(replaceState).not.toHaveBeenCalled();
+ expect(fakeWindow.location.href).toEqual('http://server');
+ });
+
+ it('should return $browser to allow chaining', function() {
+ expect(browser.url('http://any.com')).toBe(browser);
+ });
+ });
+
+ describe('urlChange', function() {
+ var callback;
+
+ beforeEach(function() {
+ callback = jasmine.createSpy('onUrlChange');
+ });
+
+ afterEach(function() {
+ if (!jQuery) jqLite(fakeWindow).dealoc();
+ });
+
+ it('should return registered callback', function() {
+ expect(browser.onUrlChange(callback)).toBe(callback);
+ });
+
+ it('should forward popstate event with new url when history supported', function() {
+ sniffer.history = true;
+ browser.onUrlChange(callback);
+ fakeWindow.location.href = 'http://server/new';
+
+ fakeWindow.fire('popstate');
+ expect(callback).toHaveBeenCalledWith('http://server/new');
+
+ fakeWindow.fire('hashchange');
+ fakeWindow.setTimeout.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should forward only popstate event when both history and hashchange supported', function() {
+ sniffer.history = true;
+ sniffer.hashchange = true;
+ browser.onUrlChange(callback);
+ fakeWindow.location.href = 'http://server/new';
+
+ fakeWindow.fire('popstate');
+ expect(callback).toHaveBeenCalledWith('http://server/new');
+
+ fakeWindow.fire('hashchange');
+ fakeWindow.setTimeout.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should forward hashchange event with new url when only hashchange supported', function() {
+ sniffer.history = false;
+ sniffer.hashchange = true;
+ browser.onUrlChange(callback);
+ fakeWindow.location.href = 'http://server/new';
+
+ fakeWindow.fire('hashchange');
+ expect(callback).toHaveBeenCalledWith('http://server/new');
+
+ fakeWindow.fire('popstate');
+ fakeWindow.setTimeout.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should use polling when neither history nor hashchange supported', function() {
+ sniffer.history = false;
+ sniffer.hashchange = false;
+ browser.onUrlChange(callback);
+
+ fakeWindow.location.href = 'http://server.new';
+ fakeWindow.setTimeout.flush();
+ expect(callback).toHaveBeenCalledWith('http://server.new');
+
+ fakeWindow.fire('popstate');
+ fakeWindow.fire('hashchange');
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should not fire urlChange if changed by browser.url method (polling)', function() {
+ sniffer.history = false;
+ sniffer.hashchange = false;
+ browser.onUrlChange(callback);
+ browser.url('http://new.com');
+
+ fakeWindow.setTimeout.flush();
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('should not fire urlChange if changed by browser.url method (hashchange)', function() {
+ sniffer.history = false;
+ sniffer.hashchange = true;
+ browser.onUrlChange(callback);
+ browser.url('http://new.com');
+
+ fakeWindow.fire('hashchange');
+ expect(callback).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('addJs', function() {
+ it('should append a script tag to body', function() {
+ browser.addJs('http://localhost/bar.js');
+ expect(scripts.length).toBe(1);
+ expect(scripts[0].src).toBe('http://localhost/bar.js');
+ expect(scripts[0].id).toBe('');
+ });
+
+ it('should return the appended script element', function() {
+ var script = browser.addJs('http://localhost/bar.js');
+ expect(script).toBe(scripts[0]);
+ });
+ });
+
+ describe('baseHref', function() {
+ var jqDocHead;
+
+ function setDocumentBaseHrefTo(href) {
+ clearDocumentBaseHref();
+ jqDocHead.append('<base href="' + href +'" />');
+ }
+
+ function clearDocumentBaseHref() {
+ jqDocHead.find('base').remove();
+ }
+
+ beforeEach(function() {
+ jqDocHead = jqLite(document).find('head');
+ });
+
+ afterEach(clearDocumentBaseHref);
+
+ it('should return value from <base href>', function() {
+ setDocumentBaseHrefTo('/base/path/');
+ expect(browser.baseHref()).toEqual('/base/path/');
+ });
+
+ it('should return undefined if no <base href>', function() {
+ expect(browser.baseHref()).toBeUndefined();
+ });
+
+ it('should remove domain from <base href>', function() {
+ setDocumentBaseHrefTo('http://host.com/base/path/');
+ expect(browser.baseHref()).toEqual('/base/path/');
+
+ setDocumentBaseHrefTo('http://host.com/base/path/index.html');
+ expect(browser.baseHref()).toEqual('/base/path/index.html');
+ });
+ });
+});
diff --git a/test/ng/cacheFactorySpec.js b/test/ng/cacheFactorySpec.js
new file mode 100644
index 00000000..dc68b63d
--- /dev/null
+++ b/test/ng/cacheFactorySpec.js
@@ -0,0 +1,317 @@
+describe('$cacheFactory', function() {
+
+ it('should be injected', inject(function($cacheFactory) {
+ expect($cacheFactory).toBeDefined();
+ }));
+
+
+ it('should return a new cache whenever called', inject(function($cacheFactory) {
+ var cache1 = $cacheFactory('cache1');
+ var cache2 = $cacheFactory('cache2');
+ expect(cache1).not.toEqual(cache2);
+ }));
+
+
+ it('should complain if the cache id is being reused', inject(function($cacheFactory) {
+ $cacheFactory('cache1');
+ expect(function() { $cacheFactory('cache1'); }).
+ toThrow('cacheId cache1 taken');
+ }));
+
+
+ describe('info', function() {
+
+ it('should provide info about all created caches', inject(function($cacheFactory) {
+ expect($cacheFactory.info()).toEqual({});
+
+ var cache1 = $cacheFactory('cache1');
+ expect($cacheFactory.info()).toEqual({cache1: {id: 'cache1', size: 0}});
+
+ cache1.put('foo', 'bar');
+ expect($cacheFactory.info()).toEqual({cache1: {id: 'cache1', size: 1}});
+ }));
+ });
+
+
+ describe('get', function() {
+
+ it('should return a cache if looked up by id', inject(function($cacheFactory) {
+ var cache1 = $cacheFactory('cache1'),
+ cache2 = $cacheFactory('cache2');
+
+ expect(cache1).not.toBe(cache2);
+ expect(cache1).toBe($cacheFactory.get('cache1'));
+ expect(cache2).toBe($cacheFactory.get('cache2'));
+ }));
+ });
+
+ describe('cache', function() {
+ var cache;
+
+ beforeEach(inject(function($cacheFactory) {
+ cache = $cacheFactory('test');
+ }));
+
+
+ describe('put, get & remove', function() {
+
+ it('should add cache entries via add and retrieve them via get', inject(function($cacheFactory) {
+ cache.put('key1', 'bar');
+ cache.put('key2', {bar:'baz'});
+
+ expect(cache.get('key2')).toEqual({bar:'baz'});
+ expect(cache.get('key1')).toBe('bar');
+ }));
+
+
+ it('should ignore put if the value is undefined', inject(function($cacheFactory) {
+ cache.put();
+ cache.put('key1');
+ cache.put('key2', undefined);
+
+ expect(cache.info().size).toBe(0);
+ }));
+
+
+ it('should remove entries via remove', inject(function($cacheFactory) {
+ cache.put('k1', 'foo');
+ cache.put('k2', 'bar');
+
+ cache.remove('k2');
+
+ expect(cache.get('k1')).toBe('foo');
+ expect(cache.get('k2')).toBeUndefined();
+
+ cache.remove('k1');
+
+ expect(cache.get('k1')).toBeUndefined();
+ expect(cache.get('k2')).toBeUndefined();
+ }));
+
+
+ it('should stringify keys', inject(function($cacheFactory) {
+ cache.put('123', 'foo');
+ cache.put(123, 'bar');
+
+ expect(cache.get('123')).toBe('bar');
+ expect(cache.info().size).toBe(1);
+
+ cache.remove(123);
+ expect(cache.info().size).toBe(0);
+ }));
+ });
+
+
+ describe('info', function() {
+
+ it('should size increment with put and decrement with remove', inject(function($cacheFactory) {
+ expect(cache.info().size).toBe(0);
+
+ cache.put('foo', 'bar');
+ expect(cache.info().size).toBe(1);
+
+ cache.put('baz', 'boo');
+ expect(cache.info().size).toBe(2);
+
+ cache.remove('baz');
+ expect(cache.info().size).toBe(1);
+
+ cache.remove('foo');
+ expect(cache.info().size).toBe(0);
+ }));
+
+
+ it('should return cache id', inject(function($cacheFactory) {
+ expect(cache.info().id).toBe('test');
+ }));
+ });
+
+
+ describe('removeAll', function() {
+
+ it('should blow away all data', inject(function($cacheFactory) {
+ cache.put('id1', 1);
+ cache.put('id2', 2);
+ cache.put('id3', 3);
+ expect(cache.info().size).toBe(3);
+
+ cache.removeAll();
+
+ expect(cache.info().size).toBe(0);
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBeUndefined();
+ expect(cache.get('id3')).toBeUndefined();
+ }));
+ });
+
+
+ describe('destroy', function() {
+
+ it('should make the cache unusable and remove references to it from $cacheFactory', inject(function($cacheFactory) {
+ cache.put('foo', 'bar');
+ cache.destroy();
+
+ expect(function() { cache.get('foo'); } ).toThrow();
+ expect(function() { cache.get('neverexisted'); }).toThrow();
+ expect(function() { cache.put('foo', 'bar'); }).toThrow();
+
+ expect($cacheFactory.get('test')).toBeUndefined();
+ expect($cacheFactory.info()).toEqual({});
+ }));
+ });
+ });
+
+
+ describe('LRU cache', function() {
+
+ it('should create cache with defined capacity', inject(function($cacheFactory) {
+ cache = $cacheFactory('cache1', {capacity: 5});
+ expect(cache.info().size).toBe(0);
+
+ for (var i=0; i<5; i++) {
+ cache.put('id' + i, i);
+ }
+
+ expect(cache.info().size).toBe(5);
+
+ cache.put('id5', 5);
+ expect(cache.info().size).toBe(5);
+ cache.put('id6', 6);
+ expect(cache.info().size).toBe(5);
+ }));
+
+
+ describe('eviction', function() {
+
+ beforeEach(inject(function($cacheFactory) {
+ cache = $cacheFactory('cache1', {capacity: 2});
+
+ cache.put('id0', 0);
+ cache.put('id1', 1);
+ }));
+
+
+ it('should kick out the first entry on put', inject(function($cacheFactory) {
+ cache.put('id2', 2);
+ expect(cache.get('id0')).toBeUndefined();
+ expect(cache.get('id1')).toBe(1);
+ expect(cache.get('id2')).toBe(2);
+ }));
+
+
+ it('should refresh an entry via get', inject(function($cacheFactory) {
+ cache.get('id0');
+ cache.put('id2', 2);
+ expect(cache.get('id0')).toBe(0);
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBe(2);
+ }));
+
+
+ it('should refresh an entry via put', inject(function($cacheFactory) {
+ cache.put('id0', '00');
+ cache.put('id2', 2);
+ expect(cache.get('id0')).toBe('00');
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBe(2);
+ }));
+
+
+ it('should not purge an entry if another one was removed', inject(function($cacheFactory) {
+ cache.remove('id1');
+ cache.put('id2', 2);
+ expect(cache.get('id0')).toBe(0);
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBe(2);
+ }));
+
+
+ it('should purge the next entry if the stalest one was removed', inject(function($cacheFactory) {
+ cache.remove('id0');
+ cache.put('id2', 2);
+ cache.put('id3', 3);
+ expect(cache.get('id0')).toBeUndefined();
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBe(2);
+ expect(cache.get('id3')).toBe(3);
+ }));
+
+
+ it('should correctly recreate the linked list if all cache entries were removed', inject(function($cacheFactory) {
+ cache.remove('id0');
+ cache.remove('id1');
+ cache.put('id2', 2);
+ cache.put('id3', 3);
+ cache.put('id4', 4);
+ expect(cache.get('id0')).toBeUndefined();
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBeUndefined();
+ expect(cache.get('id3')).toBe(3);
+ expect(cache.get('id4')).toBe(4);
+ }));
+
+
+ it('should blow away the entire cache via removeAll and start evicting when full', inject(function($cacheFactory) {
+ cache.put('id0', 0);
+ cache.put('id1', 1);
+ cache.removeAll();
+
+ cache.put('id2', 2);
+ cache.put('id3', 3);
+ cache.put('id4', 4);
+
+ expect(cache.info().size).toBe(2);
+ expect(cache.get('id0')).toBeUndefined();
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBeUndefined();
+ expect(cache.get('id3')).toBe(3);
+ expect(cache.get('id4')).toBe(4);
+ }));
+
+
+ it('should correctly refresh and evict items if operations are chained', inject(function($cacheFactory) {
+ cache = $cacheFactory('cache2', {capacity: 3});
+
+ cache.put('id0', 0); //0
+ cache.put('id1', 1); //1,0
+ cache.put('id2', 2); //2,1,0
+ cache.get('id0'); //0,2,1
+ cache.put('id3', 3); //3,0,2
+ cache.put('id0', 9); //0,3,2
+ cache.put('id4', 4); //4,0,3
+
+ expect(cache.get('id3')).toBe(3);
+ expect(cache.get('id0')).toBe(9);
+ expect(cache.get('id4')).toBe(4);
+
+ cache.remove('id0'); //4,3
+ cache.remove('id3'); //4
+ cache.put('id5', 5); //5,4
+ cache.put('id6', 6); //6,5,4
+ cache.get('id4'); //4,6,5
+ cache.put('id7', 7); //7,4,6
+
+ expect(cache.get('id0')).toBeUndefined();
+ expect(cache.get('id1')).toBeUndefined();
+ expect(cache.get('id2')).toBeUndefined();
+ expect(cache.get('id3')).toBeUndefined();
+ expect(cache.get('id4')).toBe(4);
+ expect(cache.get('id5')).toBeUndefined();
+ expect(cache.get('id6')).toBe(6);
+ expect(cache.get('id7')).toBe(7);
+
+ cache.removeAll();
+ cache.put('id0', 0); //0
+ cache.put('id1', 1); //1,0
+ cache.put('id2', 2); //2,1,0
+ cache.put('id3', 3); //3,2,1
+
+ expect(cache.info().size).toBe(3);
+ expect(cache.get('id0')).toBeUndefined();
+ expect(cache.get('id1')).toBe(1);
+ expect(cache.get('id2')).toBe(2);
+ expect(cache.get('id3')).toBe(3);
+ }));
+ });
+ });
+});
diff --git a/test/ng/compilerSpec.js b/test/ng/compilerSpec.js
new file mode 100644
index 00000000..dc2e20cf
--- /dev/null
+++ b/test/ng/compilerSpec.js
@@ -0,0 +1,1811 @@
+'use strict';
+
+describe('$compile', function() {
+ var element;
+
+ beforeEach(module(provideLog, function($provide, $compileProvider){
+ element = null;
+
+ $compileProvider.directive('log', function(log) {
+ return {
+ restrict: 'CAM',
+ priority:0,
+ compile: valueFn(function(scope, element, attrs) {
+ log(attrs.log || 'LOG');
+ })
+ };
+ });
+
+ $compileProvider.directive('highLog', function(log) {
+ return { restrict: 'CAM', priority:3, compile: valueFn(function(scope, element, attrs) {
+ log(attrs.highLog || 'HIGH');
+ })};
+ });
+
+ $compileProvider.directive('mediumLog', function(log) {
+ return { restrict: 'CAM', priority:2, compile: valueFn(function(scope, element, attrs) {
+ log(attrs.mediumLog || 'MEDIUM');
+ })};
+ });
+
+ $compileProvider.directive('greet', function() {
+ return { restrict: 'CAM', priority:10, compile: valueFn(function(scope, element, attrs) {
+ element.text("Hello " + attrs.greet);
+ })};
+ });
+
+ $compileProvider.directive('set', function() {
+ return function(scope, element, attrs) {
+ element.text(attrs.set);
+ };
+ });
+
+ $compileProvider.directive('mediumStop', valueFn({
+ priority: 2,
+ terminal: true
+ }));
+
+ $compileProvider.directive('stop', valueFn({
+ terminal: true
+ }));
+
+ $compileProvider.directive('negativeStop', valueFn({
+ priority: -100, // even with negative priority we still should be able to stop descend
+ terminal: true
+ }));
+ }));
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ describe('configuration', function() {
+ it('should register a directive', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('div', function(log) {
+ return {
+ restrict: 'ECA',
+ link: function(scope, element) {
+ log('OK');
+ element.text('SUCCESS');
+ }
+ };
+ })
+ });
+ inject(function($compile, $rootScope, log) {
+ element = $compile('<div></div>')($rootScope);
+ expect(element.text()).toEqual('SUCCESS');
+ expect(log).toEqual('OK');
+ })
+ });
+
+ it('should allow registration of multiple directives with same name', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('div', function(log) {
+ return {
+ restrict: 'ECA',
+ link: log.fn('1')
+ };
+ });
+ $compileProvider.directive('div', function(log) {
+ return {
+ restrict: 'ECA',
+ link: log.fn('2')
+ };
+ });
+ });
+ inject(function($compile, $rootScope, log) {
+ element = $compile('<div></div>')($rootScope);
+ expect(log).toEqual('1; 2');
+ });
+ });
+ });
+
+
+ describe('compile phase', function() {
+
+ it('should wrap root text nodes in spans', inject(function($compile, $rootScope) {
+ element = jqLite('<div>A&lt;a&gt;B&lt;/a&gt;C</div>');
+ var text = element.contents();
+ expect(text[0].nodeName).toEqual('#text');
+ text = $compile(text)($rootScope);
+ expect(lowercase(text[0].nodeName)).toEqual('span');
+ expect(element.find('span').text()).toEqual('A<a>B</a>C');
+ }));
+
+ describe('multiple directives per element', function() {
+ it('should allow multiple directives per element', inject(function($compile, $rootScope, log){
+ element = $compile(
+ '<span greet="angular" log="L" x-high-log="H" data-medium-log="M"></span>')
+ ($rootScope);
+ expect(element.text()).toEqual('Hello angular');
+ expect(log).toEqual('H; M; L');
+ }));
+
+
+ it('should recurse to children', inject(function($compile, $rootScope){
+ element = $compile('<div>0<a set="hello">1</a>2<b set="angular">3</b>4</div>')($rootScope);
+ expect(element.text()).toEqual('0hello2angular4');
+ }));
+
+
+ it('should allow directives in classes', inject(function($compile, $rootScope, log) {
+ element = $compile('<div class="greet: angular; log:123;"></div>')($rootScope);
+ expect(element.html()).toEqual('Hello angular');
+ expect(log).toEqual('123');
+ }));
+
+
+ it('should ignore not set CSS classes on SVG elements', inject(function($compile, $rootScope, log) {
+ if (!window.SVGElement) return;
+ // According to spec SVG element className property is readonly, but only FF
+ // implements it this way which causes compile exceptions.
+ element = $compile('<svg><text>{{1}}</text></svg>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('1');
+ }));
+
+
+ it('should allow directives in comments', inject(
+ function($compile, $rootScope, log) {
+ element = $compile('<div>0<!-- directive: log angular -->1</div>')($rootScope);
+ expect(log).toEqual('angular');
+ }
+ ));
+
+
+ it('should receive scope, element, and attributes', function() {
+ var injector;
+ module(function($compileProvider) {
+ $compileProvider.directive('log', function($injector, $rootScope) {
+ injector = $injector;
+ return {
+ restrict: 'CA',
+ compile: function(element, templateAttr) {
+ expect(typeof templateAttr.$normalize).toBe('function');
+ expect(typeof templateAttr.$set).toBe('function');
+ expect(isElement(templateAttr.$element)).toBeTruthy();
+ expect(element.text()).toEqual('unlinked');
+ expect(templateAttr.exp).toEqual('abc');
+ expect(templateAttr.aa).toEqual('A');
+ expect(templateAttr.bb).toEqual('B');
+ expect(templateAttr.cc).toEqual('C');
+ return function(scope, element, attr) {
+ expect(element.text()).toEqual('unlinked');
+ expect(attr).toBe(templateAttr);
+ expect(scope).toEqual($rootScope);
+ element.text('worked');
+ }
+ }
+ };
+ });
+ });
+ inject(function($rootScope, $compile, $injector) {
+ element = $compile(
+ '<div class="log" exp="abc" aa="A" x-Bb="B" daTa-cC="C">unlinked</div>')($rootScope);
+ expect(element.text()).toEqual('worked');
+ expect(injector).toBe($injector); // verify that directive is injectable
+ });
+ });
+ });
+
+ describe('error handling', function() {
+
+ it('should handle exceptions', function() {
+ module(function($compileProvider, $exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ $compileProvider.directive('factoryError', function() { throw 'FactoryError'; });
+ $compileProvider.directive('templateError',
+ valueFn({ compile: function() { throw 'TemplateError'; } }));
+ $compileProvider.directive('linkingError',
+ valueFn(function() { throw 'LinkingError'; }));
+ });
+ inject(function($rootScope, $compile, $exceptionHandler) {
+ element = $compile('<div factory-error template-error linking-error></div>')($rootScope);
+ expect($exceptionHandler.errors[0]).toEqual('FactoryError');
+ expect($exceptionHandler.errors[1][0]).toEqual('TemplateError');
+ expect(ie($exceptionHandler.errors[1][1])).
+ toEqual('<div factory-error linking-error template-error>');
+ expect($exceptionHandler.errors[2][0]).toEqual('LinkingError');
+ expect(ie($exceptionHandler.errors[2][1])).
+ toEqual('<div class="ng-scope" factory-error linking-error template-error>');
+
+
+ // crazy stuff to make IE happy
+ function ie(text) {
+ var list = [],
+ parts, elementName;
+
+ parts = lowercase(text).
+ replace('<', '').
+ replace('>', '').
+ split(' ');
+ elementName = parts.shift();
+ parts.sort();
+ parts.unshift(elementName);
+ forEach(parts, function(value, key){
+ if (value.substring(0,3) == 'ng-') {
+ } else {
+ value = value.replace('=""', '');
+ var match = value.match(/=(.*)/);
+ if (match && match[1].charAt(0) != '"') {
+ value = value.replace(/=(.*)/, '="$1"');
+ }
+ list.push(value);
+ }
+ });
+ return '<' + list.join(' ') + '>';
+ }
+ });
+ });
+
+
+ it('should prevent changing of structure', inject(
+ function($compile, $rootScope){
+ element = jqLite("<div><div log></div></div>");
+ var linkFn = $compile(element);
+ element.append("<div></div>");
+ expect(function() {
+ linkFn($rootScope);
+ }).toThrow('Template changed structure!');
+ }
+ ));
+ });
+
+ describe('compiler control', function() {
+ describe('priority', function() {
+ it('should honor priority', inject(function($compile, $rootScope, log){
+ element = $compile(
+ '<span log="L" x-high-log="H" data-medium-log="M"></span>')
+ ($rootScope);
+ expect(log).toEqual('H; M; L');
+ }));
+ });
+
+
+ describe('terminal', function() {
+
+ it('should prevent further directives from running', inject(function($rootScope, $compile) {
+ element = $compile('<div negative-stop><a set="FAIL">OK</a></div>')($rootScope);
+ expect(element.text()).toEqual('OK');
+ }
+ ));
+
+
+ it('should prevent further directives from running, but finish current priority level',
+ inject(function($rootScope, $compile, log) {
+ // class is processed after attrs, so putting log in class will put it after
+ // the stop in the current level. This proves that the log runs after stop
+ element = $compile(
+ '<div high-log medium-stop log class="medium-log"><a set="FAIL">OK</a></div>')($rootScope);
+ expect(element.text()).toEqual('OK');
+ expect(log.toArray().sort()).toEqual(['HIGH', 'MEDIUM']);
+ })
+ );
+ });
+
+
+ describe('restrict', function() {
+
+ it('should allow restriction of attributes', function() {
+ module(function($compileProvider, $provide) {
+ forEach({div:'E', attr:'A', clazz:'C', all:'EAC'}, function(restrict, name) {
+ $compileProvider.directive(name, function(log) {
+ return {
+ restrict: restrict,
+ compile: valueFn(function(scope, element, attr) {
+ log(name);
+ })
+ };
+ });
+ });
+ });
+ inject(function($rootScope, $compile, log) {
+ dealoc($compile('<span div class="div"></span>')($rootScope));
+ expect(log).toEqual('');
+ log.reset();
+
+ dealoc($compile('<div></div>')($rootScope));
+ expect(log).toEqual('div');
+ log.reset();
+
+ dealoc($compile('<attr class=""attr"></attr>')($rootScope));
+ expect(log).toEqual('');
+ log.reset();
+
+ dealoc($compile('<span attr></span>')($rootScope));
+ expect(log).toEqual('attr');
+ log.reset();
+
+ dealoc($compile('<clazz clazz></clazz>')($rootScope));
+ expect(log).toEqual('');
+ log.reset();
+
+ dealoc($compile('<span class="clazz"></span>')($rootScope));
+ expect(log).toEqual('clazz');
+ log.reset();
+
+ dealoc($compile('<all class="all" all></all>')($rootScope));
+ expect(log).toEqual('all; all; all');
+ });
+ });
+ });
+
+
+ describe('template', function() {
+
+
+ beforeEach(module(function($compileProvider) {
+ $compileProvider.directive('replace', valueFn({
+ restrict: 'CAM',
+ replace: true,
+ template: '<div class="log" style="width: 10px" high-log>Hello: <<CONTENT>></div>',
+ compile: function(element, attr) {
+ attr.$set('compiled', 'COMPILED');
+ expect(element).toBe(attr.$element);
+ }
+ }));
+ $compileProvider.directive('append', valueFn({
+ restrict: 'CAM',
+ template: '<div class="log" style="width: 10px" high-log>Hello: <<CONTENT>></div>',
+ compile: function(element, attr) {
+ attr.$set('compiled', 'COMPILED');
+ expect(element).toBe(attr.$element);
+ }
+ }));
+ }));
+
+
+ it('should replace element with template', inject(function($compile, $rootScope) {
+ element = $compile('<div><div replace>content</div><div>')($rootScope);
+ expect(element.text()).toEqual('Hello: content');
+ expect(element.find('div').attr('compiled')).toEqual('COMPILED');
+ }));
+
+
+ it('should append element with template', inject(function($compile, $rootScope) {
+ element = $compile('<div><div append>content</div><div>')($rootScope);
+ expect(element.text()).toEqual('Hello: content');
+ expect(element.find('div').attr('compiled')).toEqual('COMPILED');
+ }));
+
+
+ it('should compile replace template', inject(function($compile, $rootScope, log) {
+ element = $compile('<div><div replace medium-log>{{ "angular" }}</div><div>')
+ ($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: angular');
+ // HIGH goes after MEDIUM since it executes as part of replaced template
+ expect(log).toEqual('MEDIUM; HIGH; LOG');
+ }));
+
+
+ it('should compile append template', inject(function($compile, $rootScope, log) {
+ element = $compile('<div><div append medium-log>{{ "angular" }}</div><div>')
+ ($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: angular');
+ expect(log).toEqual('HIGH; LOG; MEDIUM');
+ }));
+
+
+ it('should merge attributes including style attr', inject(function($compile, $rootScope) {
+ element = $compile(
+ '<div><div replace class="medium-log" style="height: 20px" ></div><div>')
+ ($rootScope);
+ var div = element.find('div');
+ expect(div.hasClass('medium-log')).toBe(true);
+ expect(div.hasClass('log')).toBe(true);
+ expect(div.css('width')).toBe('10px');
+ expect(div.css('height')).toBe('20px');
+ expect(div.attr('replace')).toEqual('');
+ expect(div.attr('high-log')).toEqual('');
+ }));
+
+ it('should prevent multiple templates per element', inject(function($compile) {
+ try {
+ $compile('<div><span replace class="replace"></span></div>')
+ fail();
+ } catch(e) {
+ expect(e.message).toMatch(/Multiple directives .* asking for template/);
+ }
+ }));
+
+ it('should play nice with repeater when inline', inject(function($compile, $rootScope) {
+ element = $compile(
+ '<div>' +
+ '<div ng-repeat="i in [1,2]" replace>{{i}}; </div>' +
+ '</div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: 1; Hello: 2; ');
+ }));
+
+
+ it('should play nice with repeater when append', inject(function($compile, $rootScope) {
+ element = $compile(
+ '<div>' +
+ '<div ng-repeat="i in [1,2]" append>{{i}}; </div>' +
+ '</div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: 1; Hello: 2; ');
+ }));
+
+
+ it('should merge interpolated css class', inject(function($compile, $rootScope) {
+ element = $compile('<div class="one {{cls}} three" replace></div>')($rootScope);
+
+ $rootScope.$apply(function() {
+ $rootScope.cls = 'two';
+ });
+
+ expect(element).toHaveClass('one');
+ expect(element).toHaveClass('two'); // interpolated
+ expect(element).toHaveClass('three');
+ expect(element).toHaveClass('log'); // merged from replace directive template
+ }));
+
+
+ it('should merge interpolated css class with ng-repeat',
+ inject(function($compile, $rootScope) {
+ element = $compile(
+ '<div>' +
+ '<div ng-repeat="i in [1]" class="one {{cls}} three" replace></div>' +
+ '</div>')($rootScope);
+
+ $rootScope.$apply(function() {
+ $rootScope.cls = 'two';
+ });
+
+ var child = element.find('div').eq(0);
+ expect(child).toHaveClass('one');
+ expect(child).toHaveClass('two'); // interpolated
+ expect(child).toHaveClass('three');
+ expect(child).toHaveClass('log'); // merged from replace directive template
+ }));
+ });
+
+
+ describe('async templates', function() {
+
+ beforeEach(module(
+ function($compileProvider) {
+ $compileProvider.directive('hello', valueFn({ restrict: 'CAM', templateUrl: 'hello.html' }));
+ $compileProvider.directive('cau', valueFn({ restrict: 'CAM', templateUrl:'cau.html' }));
+
+ $compileProvider.directive('cError', valueFn({
+ restrict: 'CAM',
+ templateUrl:'error.html',
+ compile: function() {
+ throw Error('cError');
+ }
+ }));
+ $compileProvider.directive('lError', valueFn({
+ restrict: 'CAM',
+ templateUrl: 'error.html',
+ compile: function() {
+ throw Error('lError');
+ }
+ }));
+
+
+ $compileProvider.directive('iHello', valueFn({
+ restrict: 'CAM',
+ replace: true,
+ templateUrl: 'hello.html'
+ }));
+ $compileProvider.directive('iCau', valueFn({
+ restrict: 'CAM',
+ replace: true,
+ templateUrl:'cau.html'
+ }));
+
+ $compileProvider.directive('iCError', valueFn({
+ restrict: 'CAM',
+ replace: true,
+ templateUrl:'error.html',
+ compile: function() {
+ throw Error('cError');
+ }
+ }));
+ $compileProvider.directive('iLError', valueFn({
+ restrict: 'CAM',
+ replace: true,
+ templateUrl: 'error.html',
+ compile: function() {
+ throw Error('lError');
+ }
+ }));
+
+ }
+ ));
+
+
+ it('should append template via $http and cache it in $templateCache', inject(
+ function($compile, $httpBackend, $templateCache, $rootScope, $browser) {
+ $httpBackend.expect('GET', 'hello.html').respond('<span>Hello!</span> World!');
+ $templateCache.put('cau.html', '<span>Cau!</span>');
+ element = $compile('<div><b class="hello">ignore</b><b class="cau">ignore</b></div>')($rootScope);
+ expect(sortedHtml(element)).
+ toEqual('<div><b class="hello"></b><b class="cau"></b></div>');
+
+ $rootScope.$digest();
+
+
+ expect(sortedHtml(element)).
+ toEqual('<div><b class="hello"></b><b class="cau"><span>Cau!</span></b></div>');
+
+ $httpBackend.flush();
+ expect(sortedHtml(element)).toEqual(
+ '<div>' +
+ '<b class="hello"><span>Hello!</span> World!</b>' +
+ '<b class="cau"><span>Cau!</span></b>' +
+ '</div>');
+ }
+ ));
+
+
+ it('should inline template via $http and cache it in $templateCache', inject(
+ function($compile, $httpBackend, $templateCache, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond('<span>Hello!</span>');
+ $templateCache.put('cau.html', '<span>Cau!</span>');
+ element = $compile('<div><b class=i-hello>ignore</b><b class=i-cau>ignore</b></div>')($rootScope);
+ expect(sortedHtml(element)).
+ toEqual('<div><b class="i-hello"></b><b class="i-cau"></b></div>');
+
+ $rootScope.$digest();
+
+
+ expect(sortedHtml(element)).
+ toEqual('<div><b class="i-hello"></b><span class="i-cau">Cau!</span></div>');
+
+ $httpBackend.flush();
+ expect(sortedHtml(element)).
+ toEqual('<div><span class="i-hello">Hello!</span><span class="i-cau">Cau!</span></div>');
+ }
+ ));
+
+
+ it('should compile, link and flush the template append', inject(
+ function($compile, $templateCache, $rootScope, $browser) {
+ $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>');
+ $rootScope.name = 'Elvis';
+ element = $compile('<div><b class="hello"></b></div>')($rootScope);
+
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('<div><b class="hello"><span>Hello, Elvis!</span></b></div>');
+ }
+ ));
+
+
+ it('should compile, link and flush the template inline', inject(
+ function($compile, $templateCache, $rootScope) {
+ $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>');
+ $rootScope.name = 'Elvis';
+ element = $compile('<div><b class=i-hello></b></div>')($rootScope);
+
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('<div><span class="i-hello">Hello, Elvis!</span></div>');
+ }
+ ));
+
+
+ it('should compile, flush and link the template append', inject(
+ function($compile, $templateCache, $rootScope) {
+ $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>');
+ $rootScope.name = 'Elvis';
+ var template = $compile('<div><b class="hello"></b></div>');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('<div><b class="hello"><span>Hello, Elvis!</span></b></div>');
+ }
+ ));
+
+
+ it('should compile, flush and link the template inline', inject(
+ function($compile, $templateCache, $rootScope) {
+ $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>');
+ $rootScope.name = 'Elvis';
+ var template = $compile('<div><b class=i-hello></b></div>');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('<div><span class="i-hello">Hello, Elvis!</span></div>');
+ }
+ ));
+
+
+ it('should resolve widgets after cloning in append mode', function() {
+ module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ });
+ inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
+ $exceptionHandler) {
+ $httpBackend.expect('GET', 'hello.html').respond('<span>{{greeting}} </span>');
+ $httpBackend.expect('GET', 'error.html').respond('<div></div>');
+ $templateCache.put('cau.html', '<span>{{name}}</span>');
+ $rootScope.greeting = 'Hello';
+ $rootScope.name = 'Elvis';
+ var template = $compile(
+ '<div>' +
+ '<b class="hello"></b>' +
+ '<b class="cau"></b>' +
+ '<b class=c-error></b>' +
+ '<b class=l-error></b>' +
+ '</div>');
+ var e1;
+ var e2;
+
+ e1 = template($rootScope.$new(), noop); // clone
+ expect(e1.text()).toEqual('');
+
+ $httpBackend.flush();
+
+ e2 = template($rootScope.$new(), noop); // clone
+ $rootScope.$digest();
+ expect(e1.text()).toEqual('Hello Elvis');
+ expect(e2.text()).toEqual('Hello Elvis');
+
+ expect($exceptionHandler.errors.length).toEqual(2);
+ expect($exceptionHandler.errors[0][0].message).toEqual('cError');
+ expect($exceptionHandler.errors[1][0].message).toEqual('lError');
+
+ dealoc(e1);
+ dealoc(e2);
+ });
+ });
+
+
+ it('should resolve widgets after cloning in inline mode', function() {
+ module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ });
+ inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
+ $exceptionHandler) {
+ $httpBackend.expect('GET', 'hello.html').respond('<span>{{greeting}} </span>');
+ $httpBackend.expect('GET', 'error.html').respond('<div></div>');
+ $templateCache.put('cau.html', '<span>{{name}}</span>');
+ $rootScope.greeting = 'Hello';
+ $rootScope.name = 'Elvis';
+ var template = $compile(
+ '<div>' +
+ '<b class=i-hello></b>' +
+ '<b class=i-cau></b>' +
+ '<b class=i-c-error></b>' +
+ '<b class=i-l-error></b>' +
+ '</div>');
+ var e1;
+ var e2;
+
+ e1 = template($rootScope.$new(), noop); // clone
+ expect(e1.text()).toEqual('');
+
+ $httpBackend.flush();
+
+ e2 = template($rootScope.$new(), noop); // clone
+ $rootScope.$digest();
+ expect(e1.text()).toEqual('Hello Elvis');
+ expect(e2.text()).toEqual('Hello Elvis');
+
+ expect($exceptionHandler.errors.length).toEqual(2);
+ expect($exceptionHandler.errors[0][0].message).toEqual('cError');
+ expect($exceptionHandler.errors[1][0].message).toEqual('lError');
+
+ dealoc(e1);
+ dealoc(e2);
+ });
+ });
+
+
+ it('should be implicitly terminal and not compile placeholder content in append', inject(
+ function($compile, $templateCache, $rootScope, log) {
+ // we can't compile the contents because that would result in a memory leak
+
+ $templateCache.put('hello.html', 'Hello!');
+ element = $compile('<div><b class="hello"><div log></div></b></div>')($rootScope);
+
+ expect(log).toEqual('');
+ }
+ ));
+
+
+ it('should be implicitly terminal and not compile placeholder content in inline', inject(
+ function($compile, $templateCache, $rootScope, log) {
+ // we can't compile the contents because that would result in a memory leak
+
+ $templateCache.put('hello.html', 'Hello!');
+ element = $compile('<div><b class=i-hello><div log></div></b></div>')($rootScope);
+
+ expect(log).toEqual('');
+ }
+ ));
+
+
+ it('should throw an error and clear element content if the template fails to load', inject(
+ function($compile, $httpBackend, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond(404, 'Not Found!');
+ element = $compile('<div><b class="hello">content</b></div>')($rootScope);
+
+ expect(function() {
+ $httpBackend.flush();
+ }).toThrow('Failed to load template: hello.html');
+ expect(sortedHtml(element)).toBe('<div><b class="hello"></b></div>');
+ }
+ ));
+
+
+ it('should prevent multiple templates per element', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('sync', valueFn({
+ restrict: 'C',
+ template: '<span></span>'
+ }));
+ $compileProvider.directive('async', valueFn({
+ restrict: 'C',
+ templateUrl: 'template.html'
+ }));
+ });
+ inject(function($compile){
+ expect(function() {
+ $compile('<div><div class="sync async"></div></div>');
+ }).toThrow('Multiple directives [sync, async] asking for template on: <'+
+ (msie <= 8 ? 'DIV' : 'div') + ' class="sync async">');
+ });
+ });
+
+
+ describe('delay compile / linking functions until after template is resolved', function(){
+ var template;
+ beforeEach(module(function($compileProvider) {
+ function directive (name, priority, options) {
+ $compileProvider.directive(name, function(log) {
+ return (extend({
+ priority: priority,
+ compile: function() {
+ log(name + '-C');
+ return function() { log(name + '-L'); }
+ }
+ }, options || {}));
+ });
+ }
+
+ directive('first', 10);
+ directive('second', 5, { templateUrl: 'second.html' });
+ directive('third', 3);
+ directive('last', 0);
+
+ directive('iFirst', 10, {replace: true});
+ directive('iSecond', 5, {replace: true, templateUrl: 'second.html' });
+ directive('iThird', 3, {replace: true});
+ directive('iLast', 0, {replace: true});
+ }));
+
+ it('should flush after link append', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('<div third>{{1+2}}</div>');
+ template = $compile('<div><span first second last></span></div>');
+ element = template($rootScope);
+ expect(log).toEqual('first-C');
+
+ log('FLUSH');
+ $httpBackend.flush();
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'first-C; FLUSH; second-C; last-C; third-C; ' +
+ 'third-L; first-L; second-L; last-L');
+
+ var span = element.find('span');
+ expect(span.attr('first')).toEqual('');
+ expect(span.attr('second')).toEqual('');
+ expect(span.find('div').attr('third')).toEqual('');
+ expect(span.attr('last')).toEqual('');
+
+ expect(span.text()).toEqual('3');
+ }));
+
+
+ it('should flush after link inline', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('<div i-third>{{1+2}}</div>');
+ template = $compile('<div><span i-first i-second i-last></span></div>');
+ element = template($rootScope);
+ expect(log).toEqual('iFirst-C');
+
+ log('FLUSH');
+ $httpBackend.flush();
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
+ 'iFirst-L; iSecond-L; iThird-L; iLast-L');
+
+ var div = element.find('div');
+ expect(div.attr('i-first')).toEqual('');
+ expect(div.attr('i-second')).toEqual('');
+ expect(div.attr('i-third')).toEqual('');
+ expect(div.attr('i-last')).toEqual('');
+
+ expect(div.text()).toEqual('3');
+ }));
+
+
+ it('should flush before link append', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('<div third>{{1+2}}</div>');
+ template = $compile('<div><span first second last></span></div>');
+ expect(log).toEqual('first-C');
+ log('FLUSH');
+ $httpBackend.flush();
+ expect(log).toEqual('first-C; FLUSH; second-C; last-C; third-C');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'first-C; FLUSH; second-C; last-C; third-C; ' +
+ 'third-L; first-L; second-L; last-L');
+
+ var span = element.find('span');
+ expect(span.attr('first')).toEqual('');
+ expect(span.attr('second')).toEqual('');
+ expect(span.find('div').attr('third')).toEqual('');
+ expect(span.attr('last')).toEqual('');
+
+ expect(span.text()).toEqual('3');
+ }));
+
+
+ it('should flush before link inline', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('<div i-third>{{1+2}}</div>');
+ template = $compile('<div><span i-first i-second i-last></span></div>');
+ expect(log).toEqual('iFirst-C');
+ log('FLUSH');
+ $httpBackend.flush();
+ expect(log).toEqual('iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
+ 'iFirst-L; iSecond-L; iThird-L; iLast-L');
+
+ var div = element.find('div');
+ expect(div.attr('i-first')).toEqual('');
+ expect(div.attr('i-second')).toEqual('');
+ expect(div.attr('i-third')).toEqual('');
+ expect(div.attr('i-last')).toEqual('');
+
+ expect(div.text()).toEqual('3');
+ }));
+ });
+
+
+ it('should check that template has root element', inject(function($compile, $httpBackend) {
+ $httpBackend.expect('GET', 'hello.html').respond('before <b>mid</b> after');
+ $compile('<div i-hello></div>');
+ expect(function(){
+ $httpBackend.flush();
+ }).toThrow('Template must have exactly one root element: before <b>mid</b> after');
+ }));
+
+
+ it('should allow multiple elements in template', inject(function($compile, $httpBackend) {
+ $httpBackend.expect('GET', 'hello.html').respond('before <b>mid</b> after');
+ element = jqLite('<div hello></div>');
+ $compile(element);
+ $httpBackend.flush();
+ expect(element.text()).toEqual('before mid after');
+ }));
+
+
+ it('should work when widget is in root element', inject(
+ function($compile, $httpBackend, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond('<span>3==<<content>></span>');
+ element = jqLite('<b class="hello">{{1+2}}</b>');
+ $compile(element)($rootScope);
+
+ $httpBackend.flush();
+ expect(element.text()).toEqual('3==3');
+ }
+ ));
+
+
+ it('should work when widget is a repeater', inject(
+ function($compile, $httpBackend, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond('<span>i=<<content>>;</span>');
+ element = jqLite('<div><b class=hello ng-repeat="i in [1,2]">{{i}}</b></div>');
+ $compile(element)($rootScope);
+
+ $httpBackend.flush();
+ expect(element.text()).toEqual('i=1;i=2;');
+ }
+ ));
+ });
+
+
+ describe('scope', function() {
+ var iscope;
+
+ beforeEach(module(function($compileProvider) {
+ forEach(['', 'a', 'b'], function(name) {
+ $compileProvider.directive('scope' + uppercase(name), function(log) {
+ return {
+ scope: true,
+ restrict: 'CA',
+ compile: function() {
+ return function (scope, element) {
+ log(scope.$id);
+ expect(element.data('$scope')).toBe(scope);
+ };
+ }
+ };
+ });
+ $compileProvider.directive('iscope' + uppercase(name), function(log) {
+ return {
+ scope: {},
+ restrict: 'CA',
+ compile: function() {
+ return function (scope, element) {
+ iscope = scope;
+ log(scope.$id);
+ expect(element.data('$scope')).toBe(scope);
+ };
+ }
+ };
+ });
+ $compileProvider.directive('tiscope' + uppercase(name), function(log) {
+ return {
+ scope: {},
+ restrict: 'CA',
+ templateUrl: 'tiscope.html',
+ compile: function() {
+ return function (scope, element) {
+ iscope = scope;
+ log(scope.$id);
+ expect(element.data('$scope')).toBe(scope);
+ };
+ }
+ };
+ });
+ });
+ $compileProvider.directive('log', function(log) {
+ return {
+ restrict: 'CA',
+ link: function(scope) {
+ log('log-' + scope.$id + '-' + scope.$parent.$id);
+ }
+ };
+ });
+ }));
+
+
+ it('should allow creation of new scopes', inject(function($rootScope, $compile, log) {
+ element = $compile('<div><span scope><a log></a></span></div>')($rootScope);
+ expect(log).toEqual('LOG; log-002-001; 002');
+ expect(element.find('span').hasClass('ng-scope')).toBe(true);
+ }));
+
+
+ it('should allow creation of new isolated scopes for directives', inject(
+ function($rootScope, $compile, log) {
+ element = $compile('<div><span iscope><a log></a></span></div>')($rootScope);
+ expect(log).toEqual('LOG; log-002-001; 002');
+ $rootScope.name = 'abc';
+ expect(iscope.$parent).toBe($rootScope);
+ expect(iscope.name).toBeUndefined();
+ }));
+
+
+ it('should allow creation of new isolated scopes for directives with templates', inject(
+ function($rootScope, $compile, log, $httpBackend) {
+ $httpBackend.expect('GET', 'tiscope.html').respond('<a log></a>');
+ element = $compile('<div><span tiscope></span></div>')($rootScope);
+ $httpBackend.flush();
+ expect(log).toEqual('LOG; log-002-001; 002');
+ $rootScope.name = 'abc';
+ expect(iscope.$parent).toBe($rootScope);
+ expect(iscope.name).toBeUndefined();
+ }));
+
+
+ it('should correctly create the scope hierachy', inject(
+ function($rootScope, $compile, log) {
+ element = $compile(
+ '<div>' + //1
+ '<b class=scope>' + //2
+ '<b class=scope><b class=log></b></b>' + //3
+ '<b class=log></b>' +
+ '</b>' +
+ '<b class=scope>' + //4
+ '<b class=log></b>' +
+ '</b>' +
+ '</div>'
+ )($rootScope);
+ expect(log).toEqual('LOG; log-003-002; 003; LOG; log-002-001; 002; LOG; log-004-001; 004');
+ })
+ );
+
+
+ it('should allow more one new scope directives per element, but directives should share' +
+ 'the scope', inject(
+ function($rootScope, $compile, log) {
+ element = $compile('<div class="scope-a; scope-b"></div>')($rootScope);
+ expect(log).toEqual('002; 002');
+ })
+ );
+
+ it('should not allow more then one isolate scope creation per element', inject(
+ function($rootScope, $compile) {
+ expect(function(){
+ $compile('<div class="iscope-a; scope-b"></div>');
+ }).toThrow('Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' +
+ '<' + (msie < 9 ? 'DIV' : 'div') +
+ ' class="iscope-a; scope-b ng-isolate-scope ng-scope">');
+ })
+ );
+
+
+ it('should not allow more then one isolate scope creation per element', inject(
+ function($rootScope, $compile) {
+ expect(function(){
+ $compile('<div class="iscope-a; iscope-b"></div>');
+ }).toThrow('Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' +
+ '<' + (msie < 9 ? 'DIV' : 'div') +
+ ' class="iscope-a; iscope-b ng-isolate-scope ng-scope">');
+ })
+ );
+
+
+ it('should create new scope even at the root of the template', inject(
+ function($rootScope, $compile, log) {
+ element = $compile('<div scope-a></div>')($rootScope);
+ expect(log).toEqual('002');
+ })
+ );
+
+
+ it('should create isolate scope even at the root of the template', inject(
+ function($rootScope, $compile, log) {
+ element = $compile('<div iscope></div>')($rootScope);
+ expect(log).toEqual('002');
+ })
+ );
+ });
+ });
+ });
+
+
+ describe('interpolation', function() {
+ var observeSpy, attrValueDuringLinking;
+
+ beforeEach(module(function($compileProvider) {
+ $compileProvider.directive('observer', function() {
+ return function(scope, elm, attr) {
+ observeSpy = jasmine.createSpy('$observe attr');
+
+ attr.$observe('someAttr', observeSpy);
+ attrValueDuringLinking = attr.someAttr;
+ };
+ });
+ }));
+
+
+ it('should compile and link both attribute and text bindings', inject(
+ function($rootScope, $compile) {
+ $rootScope.name = 'angular';
+ element = $compile('<div name="attr: {{name}}">text: {{name}}</div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('text: angular');
+ expect(element.attr('name')).toEqual('attr: angular');
+ }));
+
+
+ it('should decorate the binding with ng-binding and interpolation function', inject(
+ function($compile, $rootScope) {
+ element = $compile('<div>{{1+2}}</div>')($rootScope);
+ expect(element.hasClass('ng-binding')).toBe(true);
+ expect(element.data('$binding')[0].exp).toEqual('{{1+2}}');
+ }));
+
+
+ it('should observe interpolated attrs', inject(function($rootScope, $compile) {
+ $compile('<div some-attr="{{value}}" observer></div>')($rootScope);
+
+ // should be async
+ expect(observeSpy).not.toHaveBeenCalled();
+
+ $rootScope.$apply(function() {
+ $rootScope.value = 'bound-value';
+ });
+ expect(observeSpy).toHaveBeenCalledOnceWith('bound-value');
+ }));
+
+
+ it('should set interpolated attrs to undefined', inject(function($rootScope, $compile) {
+ attrValueDuringLinking = null;
+ $compile('<div some-attr="{{whatever}}" observer></div>')($rootScope);
+ expect(attrValueDuringLinking).toBeUndefined();
+ }));
+
+
+ it('should not call observer of non-interpolated attr', inject(function($rootScope, $compile) {
+ $compile('<div some-attr="nonBound" observer></div>')($rootScope);
+ expect(attrValueDuringLinking).toBe('nonBound');
+
+ $rootScope.$digest();
+ expect(observeSpy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should delegate exceptions to $exceptionHandler', function() {
+ observeSpy = jasmine.createSpy('$observe attr').andThrow('ERROR');
+
+ module(function($compileProvider, $exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ $compileProvider.directive('error', function() {
+ return function(scope, elm, attr) {
+ attr.$observe('someAttr', observeSpy);
+ attr.$observe('someAttr', observeSpy);
+ };
+ });
+ });
+
+ inject(function($compile, $rootScope, $exceptionHandler) {
+ $compile('<div some-attr="{{value}}" error></div>')($rootScope);
+ $rootScope.$digest();
+
+ expect(observeSpy).toHaveBeenCalled();
+ expect(observeSpy.callCount).toBe(2);
+ expect($exceptionHandler.errors).toEqual(['ERROR', 'ERROR']);
+ });
+ });
+
+
+ it('should translate {{}} in terminal nodes', inject(function($rootScope, $compile) {
+ element = $compile('<select ng:model="x"><option value="">Greet {{name}}!</option></select>')($rootScope)
+ $rootScope.$digest();
+ expect(sortedHtml(element).replace(' selected="true"', '')).
+ toEqual('<select ng:model="x">' +
+ '<option>Greet !</option>' +
+ '</select>');
+ $rootScope.name = 'Misko';
+ $rootScope.$digest();
+ expect(sortedHtml(element).replace(' selected="true"', '')).
+ toEqual('<select ng:model="x">' +
+ '<option>Greet Misko!</option>' +
+ '</select>');
+ }));
+ });
+
+
+ describe('link phase', function() {
+
+ beforeEach(module(function($compileProvider) {
+
+ forEach(['a', 'b', 'c'], function(name) {
+ $compileProvider.directive(name, function(log) {
+ return {
+ restrict: 'ECA',
+ compile: function() {
+ log('t' + uppercase(name))
+ return {
+ pre: function() {
+ log('pre' + uppercase(name));
+ },
+ post: function linkFn() {
+ log('post' + uppercase(name));
+ }
+ };
+ }
+ };
+ });
+ });
+ }));
+
+
+ it('should not store linkingFns for noop branches', inject(function ($rootScope, $compile) {
+ element = jqLite('<div name="{{a}}"><span>ignore</span></div>');
+ var linkingFn = $compile(element);
+ // Now prune the branches with no directives
+ element.find('span').remove();
+ expect(element.find('span').length).toBe(0);
+ // and we should still be able to compile without errors
+ linkingFn($rootScope);
+ }));
+
+
+ it('should compile from top to bottom but link from bottom up', inject(
+ function($compile, $rootScope, log) {
+ element = $compile('<a b><c></c></a>')($rootScope);
+ expect(log).toEqual('tA; tB; tC; preA; preB; preC; postC; postA; postB');
+ }
+ ));
+
+
+ it('should support link function on directive object', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('abc', valueFn({
+ link: function(scope, element, attrs) {
+ element.text(attrs.abc);
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('<div abc="WORKS">FAIL</div>')($rootScope);
+ expect(element.text()).toEqual('WORKS');
+ });
+ });
+ });
+
+
+ describe('attrs', function() {
+
+ it('should allow setting of attributes', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ setter: valueFn(function(scope, element, attr) {
+ attr.$set('name', 'abc');
+ attr.$set('disabled', true);
+ expect(attr.name).toBe('abc');
+ expect(attr.disabled).toBe(true);
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('<div setter></div>')($rootScope);
+ expect(element.attr('name')).toEqual('abc');
+ expect(element.attr('disabled')).toEqual('disabled');
+ });
+ });
+
+
+ it('should read boolean attributes as boolean only on control elements', function() {
+ var value;
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ input: valueFn({
+ restrict: 'ECA',
+ link:function(scope, element, attr) {
+ value = attr.required;
+ }
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('<input required></input>')($rootScope);
+ expect(value).toEqual(true);
+ });
+ });
+
+ it('should read boolean attributes as text on non-controll elements', function() {
+ var value;
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ div: valueFn({
+ restrict: 'ECA',
+ link:function(scope, element, attr) {
+ value = attr.required;
+ }
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('<div required="some text"></div>')($rootScope);
+ expect(value).toEqual('some text');
+ });
+ });
+
+ it('should allow setting of attributes', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ setter: valueFn(function(scope, element, attr) {
+ attr.$set('name', 'abc');
+ attr.$set('disabled', true);
+ expect(attr.name).toBe('abc');
+ expect(attr.disabled).toBe(true);
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('<div setter></div>')($rootScope);
+ expect(element.attr('name')).toEqual('abc');
+ expect(element.attr('disabled')).toEqual('disabled');
+ });
+ });
+
+
+ it('should create new instance of attr for each template stamping', function() {
+ module(function($compileProvider, $provide) {
+ var state = { first: [], second: [] };
+ $provide.value('state', state);
+ $compileProvider.directive({
+ first: valueFn({
+ priority: 1,
+ compile: function(templateElement, templateAttr) {
+ return function(scope, element, attr) {
+ state.first.push({
+ template: {element: templateElement, attr:templateAttr},
+ link: {element: element, attr: attr}
+ });
+ }
+ }
+ }),
+ second: valueFn({
+ priority: 2,
+ compile: function(templateElement, templateAttr) {
+ return function(scope, element, attr) {
+ state.second.push({
+ template: {element: templateElement, attr:templateAttr},
+ link: {element: element, attr: attr}
+ });
+ }
+ }
+ })
+ });
+ });
+ inject(function($rootScope, $compile, state) {
+ var template = $compile('<div first second>');
+ dealoc(template($rootScope.$new(), noop));
+ dealoc(template($rootScope.$new(), noop));
+
+ // instance between directives should be shared
+ expect(state.first[0].template.element).toBe(state.second[0].template.element);
+ expect(state.first[0].template.attr).toBe(state.second[0].template.attr);
+
+ // the template and the link can not be the same instance
+ expect(state.first[0].template.element).not.toBe(state.first[0].link.element);
+ expect(state.first[0].template.attr).not.toBe(state.first[0].link.attr);
+
+ // each new template needs to be new instance
+ expect(state.first[0].link.element).not.toBe(state.first[1].link.element);
+ expect(state.first[0].link.attr).not.toBe(state.first[1].link.attr);
+ expect(state.second[0].link.element).not.toBe(state.second[1].link.element);
+ expect(state.second[0].link.attr).not.toBe(state.second[1].link.attr);
+ });
+ });
+
+
+ describe('$set', function() {
+ var attr;
+ beforeEach(function(){
+ module(function($compileProvider) {
+ $compileProvider.directive('input', valueFn({
+ restrict: 'ECA',
+ link: function(scope, element, attr) {
+ scope.attr = attr;
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('<input></input>')($rootScope);
+ attr = $rootScope.attr;
+ expect(attr).toBeDefined();
+ });
+ });
+
+
+ it('should set attributes', function() {
+ attr.$set('ngMyAttr', 'value');
+ expect(element.attr('ng-my-attr')).toEqual('value');
+ expect(attr.ngMyAttr).toEqual('value');
+ });
+
+
+ it('should allow overriding of attribute name and remember the name', function() {
+ attr.$set('ngOther', '123', true, 'other');
+ expect(element.attr('other')).toEqual('123');
+ expect(attr.ngOther).toEqual('123');
+
+ attr.$set('ngOther', '246');
+ expect(element.attr('other')).toEqual('246');
+ expect(attr.ngOther).toEqual('246');
+ });
+
+
+ it('should remove attribute', function() {
+ attr.$set('ngMyAttr', 'value');
+ expect(element.attr('ng-my-attr')).toEqual('value');
+
+ attr.$set('ngMyAttr', undefined);
+ expect(element.attr('ng-my-attr')).toBe(undefined);
+
+ attr.$set('ngMyAttr', 'value');
+ attr.$set('ngMyAttr', null);
+ expect(element.attr('ng-my-attr')).toBe(undefined);
+ });
+
+
+ it('should not set DOM element attr if writeAttr false', function() {
+ attr.$set('test', 'value', false);
+
+ expect(element.attr('test')).toBeUndefined();
+ expect(attr.test).toBe('value');
+ });
+ });
+ });
+
+
+ describe('locals', function() {
+ it('should marshal to locals', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('widget', function(log) {
+ return {
+ scope: {
+ attr: 'attribute',
+ prop: 'evaluate',
+ bind: 'bind',
+ assign: 'accessor',
+ read: 'accessor',
+ exp: 'expression',
+ nonExist: 'accessor',
+ nonExistExpr: 'expression'
+ },
+ link: function(scope, element, attrs) {
+ scope.nonExist(); // noop
+ scope.nonExist(123); // noop
+ scope.nonExistExpr(); // noop
+ scope.nonExistExpr(123); // noop
+ log(scope.attr);
+ log(scope.prop);
+ log(scope.assign());
+ log(scope.read());
+ log(scope.assign('ng'));
+ scope.exp({myState:'OK'});
+ expect(function() { scope.read(undefined); }).
+ toThrow("Expression ''D'' not assignable.");
+ scope.$watch('bind', log);
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ $rootScope.myProp = 'B';
+ $rootScope.bi = {nd: 'C'};
+ $rootScope.name = 'C';
+ element = $compile(
+ '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' +
+ 'exp="state=myState">{{bind}}</div></div>')
+ ($rootScope);
+ expect(log).toEqual('A; B; C; D; ng');
+ expect($rootScope.name).toEqual('ng');
+ expect($rootScope.state).toEqual('OK');
+ log.reset();
+ $rootScope.$apply();
+ expect(element.text()).toEqual('C');
+ expect(log).toEqual('C');
+ $rootScope.bi.nd = 'c';
+ $rootScope.$apply();
+ expect(log).toEqual('C; c');
+ });
+ });
+ });
+
+
+ describe('controller', function() {
+ it('should inject locals to controller', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('widget', function(log) {
+ return {
+ controller: function(attr, prop, assign, read, exp){
+ log(attr);
+ log(prop);
+ log(assign());
+ log(read());
+ log(assign('ng'));
+ exp();
+ expect(function() { read(undefined); }).
+ toThrow("Expression ''D'' not assignable.");
+ this.result = 'OK';
+ },
+ inject: {
+ attr: 'attribute',
+ prop: 'evaluate',
+ assign: 'accessor',
+ read: 'accessor',
+ exp: 'expression'
+ },
+ link: function(scope, element, attrs, controller) {
+ log(controller.result);
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ $rootScope.myProp = 'B';
+ $rootScope.bi = {nd: 'C'};
+ $rootScope.name = 'C';
+ element = $compile(
+ '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' +
+ 'exp="state=\'OK\'">{{bind}}</div></div>')
+ ($rootScope);
+ expect(log).toEqual('A; B; C; D; ng; OK');
+ expect($rootScope.name).toEqual('ng');
+ });
+ });
+
+
+ it('should get required controller', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('main', function(log) {
+ return {
+ priority: 2,
+ controller: function() {
+ this.name = 'main';
+ },
+ link: function(scope, element, attrs, controller) {
+ log(controller.name);
+ }
+ };
+ });
+ $compileProvider.directive('dep', function(log) {
+ return {
+ priority: 1,
+ require: 'main',
+ link: function(scope, element, attrs, controller) {
+ log('dep:' + controller.name);
+ }
+ };
+ });
+ $compileProvider.directive('other', function(log) {
+ return {
+ link: function(scope, element, attrs, controller) {
+ log(!!controller); // should be false
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('<div main dep other></div>')($rootScope);
+ expect(log).toEqual('main; dep:main; false');
+ });
+ });
+
+
+ it('should require controller on parent element',function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('main', function(log) {
+ return {
+ controller: function() {
+ this.name = 'main';
+ }
+ };
+ });
+ $compileProvider.directive('dep', function(log) {
+ return {
+ require: '^main',
+ link: function(scope, element, attrs, controller) {
+ log('dep:' + controller.name);
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('<div main><div dep></div></div>')($rootScope);
+ expect(log).toEqual('dep:main');
+ });
+ });
+
+
+ it('should have optional controller on current element', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('dep', function(log) {
+ return {
+ require: '?main',
+ link: function(scope, element, attrs, controller) {
+ log('dep:' + !!controller);
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('<div main><div dep></div></div>')($rootScope);
+ expect(log).toEqual('dep:false');
+ });
+ });
+
+
+ it('should support multiple controllers', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('c1', valueFn({
+ controller: function() { this.name = 'c1'; }
+ }));
+ $compileProvider.directive('c2', valueFn({
+ controller: function() { this.name = 'c2'; }
+ }));
+ $compileProvider.directive('dep', function(log) {
+ return {
+ require: ['^c1', '^c2'],
+ link: function(scope, element, attrs, controller) {
+ log('dep:' + controller[0].name + '-' + controller[1].name);
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('<div c1 c2><div dep></div></div>')($rootScope);
+ expect(log).toEqual('dep:c1-c2');
+ });
+
+ });
+ });
+
+
+ describe('transclude', function() {
+ it('should compile get templateFn', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('trans', function(log) {
+ return {
+ transclude: 'element',
+ priority: 2,
+ controller: function($transclude) { this.$transclude = $transclude; },
+ compile: function(element, attrs, template) {
+ log('compile: ' + angular.mock.dump(element));
+ return function(scope, element, attrs, ctrl) {
+ log('link');
+ var cursor = element;
+ template(scope.$new(), function(clone) {cursor.after(cursor = clone)});
+ ctrl.$transclude(function(clone) {cursor.after(clone)});
+ };
+ }
+ }
+ });
+ });
+ inject(function(log, $rootScope, $compile) {
+ element = $compile('<div><div high-log trans="text" log>{{$parent.$id}}-{{$id}};</div></div>')
+ ($rootScope);
+ $rootScope.$apply();
+ expect(log).toEqual('compile: <!-- trans: text -->; HIGH; link; LOG; LOG');
+ expect(element.text()).toEqual('001-002;001-003;');
+ });
+ });
+
+
+ it('should support transclude directive', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('trans', function() {
+ return {
+ transclude: 'content',
+ replace: true,
+ scope: true,
+ template: '<ul><li>W:{{$parent.$id}}-{{$id}};</li><li ng-transclude></li></ul>'
+ }
+ });
+ });
+ inject(function(log, $rootScope, $compile) {
+ element = $compile('<div><div trans>T:{{$parent.$id}}-{{$id}}<span>;</span></div></div>')
+ ($rootScope);
+ $rootScope.$apply();
+ expect(element.text()).toEqual('W:001-002;T:001-003;');
+ expect(jqLite(element.find('span')[0]).text()).toEqual('T:001-003');
+ expect(jqLite(element.find('span')[1]).text()).toEqual(';');
+ });
+ });
+
+
+ it('should transclude transcluded content', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('book', valueFn({
+ transclude: 'content',
+ template: '<div>book-<div chapter>(<div ng-transclude></div>)</div></div>'
+ }));
+ $compileProvider.directive('chapter', valueFn({
+ transclude: 'content',
+ templateUrl: 'chapter.html'
+ }));
+ $compileProvider.directive('section', valueFn({
+ transclude: 'content',
+ template: '<div>section-!<div ng-transclude></div>!</div></div>'
+ }));
+ return function($httpBackend) {
+ $httpBackend.
+ expect('GET', 'chapter.html').
+ respond('<div>chapter-<div section>[<div ng-transclude></div>]</div></div>');
+ }
+ });
+ inject(function(log, $rootScope, $compile, $httpBackend) {
+ element = $compile('<div><div book>paragraph</div></div>')($rootScope);
+ $rootScope.$apply();
+
+ expect(element.text()).toEqual('book-');
+
+ $httpBackend.flush();
+ $rootScope.$apply();
+ expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!');
+ });
+ });
+
+
+ it('should only allow one transclude per element', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('first', valueFn({
+ scope: {},
+ restrict: 'CA',
+ transclude: 'content'
+ }));
+ $compileProvider.directive('second', valueFn({
+ restrict: 'CA',
+ transclude: 'content'
+ }));
+ });
+ inject(function($compile) {
+ expect(function() {
+ $compile('<div class="first second"></div>');
+ }).toThrow('Multiple directives [first, second] asking for transclusion on: <' +
+ (msie <= 8 ? 'DIV' : 'div') + ' class="first second ng-isolate-scope ng-scope">');
+ });
+ });
+
+
+ it('should remove transclusion scope, when the DOM is destroyed', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('box', valueFn({
+ transclude: 'content',
+ scope: { name: 'evaluate', show: 'accessor' },
+ template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>',
+ link: function(scope, element) {
+ scope.$watch(
+ function() { return scope.show(); },
+ function(show) {
+ if (!show) {
+ element.find('div').find('div').remove();
+ }
+ }
+ );
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ $rootScope.username = 'Misko';
+ $rootScope.select = true;
+ element = $compile(
+ '<div><div box name="username" show="select">user: {{username}}</div></div>')
+ ($rootScope);
+ $rootScope.$apply();
+ expect(element.text()).toEqual('Hello: Misko!user: Misko');
+
+ var widgetScope = $rootScope.$$childHead;
+ var transcludeScope = widgetScope.$$nextSibling;
+ expect(widgetScope.name).toEqual('Misko');
+ expect(widgetScope.$parent).toEqual($rootScope);
+ expect(transcludeScope.$parent).toEqual($rootScope);
+
+ $rootScope.select = false;
+ $rootScope.$apply();
+ expect(element.text()).toEqual('Hello: Misko!');
+ expect(widgetScope.$$nextSibling).toEqual(null);
+ });
+ });
+
+
+ it('should support transcluded element on root content', function() {
+ var comment;
+ module(function($compileProvider) {
+ $compileProvider.directive('transclude', valueFn({
+ transclude: 'element',
+ compile: function(element, attr, linker) {
+ return function(scope, element, attr) {
+ comment = element;
+ };
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ var element = jqLite('<div>before<div transclude></div>after</div>').contents();
+ expect(element.length).toEqual(3);
+ expect(nodeName_(element[1])).toBe('DIV');
+ $compile(element)($rootScope);
+ expect(nodeName_(element[1])).toBe('#comment');
+ expect(nodeName_(comment)).toBe('#comment');
+ });
+ });
+ });
+});
diff --git a/test/ng/controllerSpec.js b/test/ng/controllerSpec.js
new file mode 100644
index 00000000..91389013
--- /dev/null
+++ b/test/ng/controllerSpec.js
@@ -0,0 +1,73 @@
+'use strict';
+
+describe('$controller', function() {
+ var $controllerProvider, $controller;
+
+ beforeEach(module(function(_$controllerProvider_) {
+ $controllerProvider = _$controllerProvider_;
+ }));
+
+
+ beforeEach(inject(function(_$controller_) {
+ $controller = _$controller_;
+ }));
+
+
+ describe('provider', function() {
+
+ it('should allow registration of controllers', function() {
+ var FooCtrl = function($scope) { $scope.foo = 'bar' },
+ scope = {},
+ ctrl;
+
+ $controllerProvider.register('FooCtrl', FooCtrl);
+ ctrl = $controller('FooCtrl', {$scope: scope});
+
+ expect(scope.foo).toBe('bar');
+ expect(ctrl instanceof FooCtrl).toBe(true);
+ });
+
+
+ it('should allow registration of controllers annotated with arrays', function() {
+ var FooCtrl = function($scope) { $scope.foo = 'bar' },
+ scope = {},
+ ctrl;
+
+ $controllerProvider.register('FooCtrl', ['$scope', FooCtrl]);
+ ctrl = $controller('FooCtrl', {$scope: scope});
+
+ expect(scope.foo).toBe('bar');
+ expect(ctrl instanceof FooCtrl).toBe(true);
+ });
+ });
+
+
+ it('should return instance of given controller class', function() {
+ var MyClass = function() {},
+ ctrl = $controller(MyClass);
+
+ expect(ctrl).toBeDefined();
+ expect(ctrl instanceof MyClass).toBe(true);
+ });
+
+ it('should inject arguments', inject(function($http) {
+ var MyClass = function($http) {
+ this.$http = $http;
+ };
+
+ var ctrl = $controller(MyClass);
+ expect(ctrl.$http).toBe($http);
+ }));
+
+
+ it('should inject given scope', function() {
+ var MyClass = function($scope) {
+ this.$scope = $scope;
+ };
+
+ var scope = {},
+ ctrl = $controller(MyClass, {$scope: scope});
+
+ expect(ctrl.$scope).toBe(scope);
+ });
+});
diff --git a/test/ng/cookieStoreSpec.js b/test/ng/cookieStoreSpec.js
new file mode 100644
index 00000000..50ac7797
--- /dev/null
+++ b/test/ng/cookieStoreSpec.js
@@ -0,0 +1,30 @@
+'use strict';
+
+describe('$cookieStore', function() {
+
+
+ it('should serialize objects to json', inject(function($cookieStore, $browser, $rootScope) {
+ $cookieStore.put('objectCookie', {id: 123, name: 'blah'});
+ $rootScope.$digest();
+ expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'});
+ }));
+
+
+ it('should deserialize json to object', inject(function($cookieStore, $browser) {
+ $browser.cookies('objectCookie', '{"id":123,"name":"blah"}');
+ $browser.poll();
+ expect($cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'});
+ }));
+
+
+ it('should delete objects from the store when remove is called', inject(function($cookieStore, $browser, $rootScope) {
+ $cookieStore.put('gonner', { "I'll":"Be Back"});
+ $rootScope.$digest(); //force eval in test
+ $browser.poll();
+ expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'});
+
+ $cookieStore.remove('gonner');
+ $rootScope.$digest();
+ expect($browser.cookies()).toEqual({});
+ }));
+});
diff --git a/test/ng/cookiesSpec.js b/test/ng/cookiesSpec.js
new file mode 100644
index 00000000..5427ac36
--- /dev/null
+++ b/test/ng/cookiesSpec.js
@@ -0,0 +1,97 @@
+'use strict';
+
+describe('$cookies', function() {
+ beforeEach(module(function($provide) {
+ $provide.factory('$browser', function(){
+ return angular.extend(new angular.mock.$Browser(), {cookieHash: {preexisting:'oldCookie'}});
+ });
+ }));
+
+
+ it('should provide access to existing cookies via object properties and keep them in sync',
+ inject(function($cookies, $browser, $rootScope) {
+ expect($cookies).toEqual({'preexisting': 'oldCookie'});
+
+ // access internal cookie storage of the browser mock directly to simulate behavior of
+ // document.cookie
+ $browser.cookieHash['brandNew'] = 'cookie';
+ $browser.poll();
+
+ expect($cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie'});
+
+ $browser.cookieHash['brandNew'] = 'cookie2';
+ $browser.poll();
+ expect($cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie2'});
+
+ delete $browser.cookieHash['brandNew'];
+ $browser.poll();
+ expect($cookies).toEqual({'preexisting': 'oldCookie'});
+ }));
+
+
+ it('should create or update a cookie when a value is assigned to a property',
+ inject(function($cookies, $browser, $rootScope) {
+ $cookies.oatmealCookie = 'nom nom';
+ $rootScope.$digest();
+
+ expect($browser.cookies()).
+ toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});
+
+ $cookies.oatmealCookie = 'gone';
+ $rootScope.$digest();
+
+ expect($browser.cookies()).
+ toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'});
+ }));
+
+
+ it('should drop or reset any cookie that was set to a non-string value',
+ inject(function($cookies, $browser, $rootScope) {
+ $cookies.nonString = [1, 2, 3];
+ $cookies.nullVal = null;
+ $cookies.undefVal = undefined;
+ $cookies.preexisting = function() {};
+ $rootScope.$digest();
+ expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
+ expect($cookies).toEqual({'preexisting': 'oldCookie'});
+ }));
+
+
+ it('should remove a cookie when a $cookies property is deleted',
+ inject(function($cookies, $browser, $rootScope) {
+ $cookies.oatmealCookie = 'nom nom';
+ $rootScope.$digest();
+ $browser.poll();
+ expect($browser.cookies()).
+ toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'});
+
+ delete $cookies.oatmealCookie;
+ $rootScope.$digest();
+
+ expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'});
+ }));
+
+
+ it('should drop or reset cookies that browser refused to store',
+ inject(function($cookies, $browser, $rootScope) {
+ var i, longVal;
+
+ for (i=0; i<5000; i++) {
+ longVal += '*';
+ }
+
+ //drop if no previous value
+ $cookies.longCookie = longVal;
+ $rootScope.$digest();
+ expect($cookies).toEqual({'preexisting': 'oldCookie'});
+
+
+ //reset if previous value existed
+ $cookies.longCookie = 'shortVal';
+ $rootScope.$digest();
+ expect($cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
+ $cookies.longCookie = longVal;
+ $rootScope.$digest();
+ expect($cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'});
+ }));
+});
diff --git a/test/ng/deferSpec.js b/test/ng/deferSpec.js
new file mode 100644
index 00000000..48c9e912
--- /dev/null
+++ b/test/ng/deferSpec.js
@@ -0,0 +1,113 @@
+'use strict';
+
+describe('$defer', function() {
+ beforeEach(module(function($provide) {
+ $provide.factory('$exceptionHandler', function(){
+ return jasmine.createSpy('$exceptionHandler');
+ });
+ }));
+
+
+ it('should delegate functions to $browser.defer', inject(function($defer, $browser, $exceptionHandler) {
+ var counter = 0;
+ $defer(function() { counter++; });
+
+ expect(counter).toBe(0);
+
+ $browser.defer.flush();
+ expect(counter).toBe(1);
+
+ expect(function() {$browser.defer.flush();}).toThrow('No deferred tasks to be flushed');
+ expect(counter).toBe(1);
+
+ expect($exceptionHandler).not.toHaveBeenCalled();
+ }));
+
+
+ it('should delegate exception to the $exceptionHandler service', inject(function($defer, $browser, $exceptionHandler) {
+ $defer(function() {throw "Test Error";});
+ expect($exceptionHandler).not.toHaveBeenCalled();
+
+ $browser.defer.flush();
+ expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
+ }));
+
+
+ it('should call $apply after each callback is executed', inject(function($defer, $browser, $rootScope) {
+ var applySpy = this.spyOn($rootScope, '$apply').andCallThrough();
+
+ $defer(function() {});
+ expect(applySpy).not.toHaveBeenCalled();
+
+ $browser.defer.flush();
+ expect(applySpy).toHaveBeenCalled();
+
+ applySpy.reset(); //reset the spy;
+
+ $defer(function() {});
+ $defer(function() {});
+ $browser.defer.flush();
+ expect(applySpy.callCount).toBe(2);
+ }));
+
+
+ it('should call $apply even if an exception is thrown in callback', inject(function($defer, $browser, $rootScope) {
+ var applySpy = this.spyOn($rootScope, '$apply').andCallThrough();
+
+ $defer(function() {throw "Test Error";});
+ expect(applySpy).not.toHaveBeenCalled();
+
+ $browser.defer.flush();
+ expect(applySpy).toHaveBeenCalled();
+ }));
+
+
+ it('should allow you to specify the delay time', inject(function($defer, $browser) {
+ var defer = this.spyOn($browser, 'defer');
+ $defer(noop, 123);
+ expect(defer.callCount).toEqual(1);
+ expect(defer.mostRecentCall.args[1]).toEqual(123);
+ }));
+
+
+ it('should return a cancelation token', inject(function($defer, $browser) {
+ var defer = this.spyOn($browser, 'defer').andReturn('xxx');
+ expect($defer(noop)).toEqual('xxx');
+ }));
+
+
+ describe('cancel', function() {
+ it('should cancel tasks', inject(function($defer, $browser) {
+ var task1 = jasmine.createSpy('task1'),
+ task2 = jasmine.createSpy('task2'),
+ task3 = jasmine.createSpy('task3'),
+ token1, token3;
+
+ token1 = $defer(task1);
+ $defer(task2);
+ token3 = $defer(task3, 333);
+
+ $defer.cancel(token3);
+ $defer.cancel(token1);
+ $browser.defer.flush();
+
+ expect(task1).not.toHaveBeenCalled();
+ expect(task2).toHaveBeenCalledOnce();
+ expect(task3).not.toHaveBeenCalled();
+ }));
+
+
+ it('should return true if a task was succesffuly canceled', inject(function($defer, $browser) {
+ var task1 = jasmine.createSpy('task1'),
+ task2 = jasmine.createSpy('task2'),
+ token1, token2;
+
+ token1 = $defer(task1);
+ $browser.defer.flush();
+ token2 = $defer(task2);
+
+ expect($defer.cancel(token1)).toBe(false);
+ expect($defer.cancel(token2)).toBe(true);
+ }));
+ });
+});
diff --git a/test/ng/directive/aSpec.js b/test/ng/directive/aSpec.js
new file mode 100644
index 00000000..8aa2449d
--- /dev/null
+++ b/test/ng/directive/aSpec.js
@@ -0,0 +1,46 @@
+'use strict';
+
+describe('a', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ it('should prevent default action to be executed when href is empty',
+ inject(function($rootScope, $compile) {
+ var orgLocation = document.location.href,
+ preventDefaultCalled = false,
+ event;
+
+ element = $compile('<a href="">empty link</a>')($rootScope);
+
+ if (msie < 9) {
+
+ event = document.createEventObject();
+ expect(event.returnValue).not.toBeDefined();
+ element[0].fireEvent('onclick', event);
+ expect(event.returnValue).toEqual(false);
+
+ } else {
+
+ event = document.createEvent('MouseEvent');
+ event.initMouseEvent(
+ 'click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+
+ event.preventDefaultOrg = event.preventDefault;
+ event.preventDefault = function() {
+ preventDefaultCalled = true;
+ if (this.preventDefaultOrg) this.preventDefaultOrg();
+ };
+
+ element[0].dispatchEvent(event);
+
+ expect(preventDefaultCalled).toEqual(true);
+ }
+
+ expect(document.location.href).toEqual(orgLocation);
+ }));
+});
diff --git a/test/ng/directive/booleanAttrDirSpecs.js b/test/ng/directive/booleanAttrDirSpecs.js
new file mode 100644
index 00000000..7a4244a8
--- /dev/null
+++ b/test/ng/directive/booleanAttrDirSpecs.js
@@ -0,0 +1,125 @@
+'use strict';
+
+describe('boolean attr directives', function() {
+ var element;
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should bind href', inject(function($rootScope, $compile) {
+ element = $compile('<a ng-href="{{url}}"></a>')($rootScope)
+ $rootScope.url = 'http://server'
+ $rootScope.$digest();
+ expect(element.attr('href')).toEqual('http://server');
+ }));
+
+
+ it('should bind disabled', inject(function($rootScope, $compile) {
+ element = $compile('<button ng-disabled="isDisabled">Button</button>')($rootScope)
+ $rootScope.isDisabled = false;
+ $rootScope.$digest();
+ expect(element.attr('disabled')).toBeFalsy();
+ $rootScope.isDisabled = true;
+ $rootScope.$digest();
+ expect(element.attr('disabled')).toBeTruthy();
+ }));
+
+
+ it('should bind checked', inject(function($rootScope, $compile) {
+ element = $compile('<input type="checkbox" ng-checked="isChecked" />')($rootScope)
+ $rootScope.isChecked = false;
+ $rootScope.$digest();
+ expect(element.attr('checked')).toBeFalsy();
+ $rootScope.isChecked=true;
+ $rootScope.$digest();
+ expect(element.attr('checked')).toBeTruthy();
+ }));
+
+
+ it('should bind selected', inject(function($rootScope, $compile) {
+ element = $compile('<select><option value=""></option><option ng-selected="isSelected">Greetings!</option></select>')($rootScope)
+ jqLite(document.body).append(element)
+ $rootScope.isSelected=false;
+ $rootScope.$digest();
+ expect(element.children()[1].selected).toBeFalsy();
+ $rootScope.isSelected=true;
+ $rootScope.$digest();
+ expect(element.children()[1].selected).toBeTruthy();
+ }));
+
+
+ it('should bind readonly', inject(function($rootScope, $compile) {
+ element = $compile('<input type="text" ng-readonly="isReadonly" />')($rootScope)
+ $rootScope.isReadonly=false;
+ $rootScope.$digest();
+ expect(element.attr('readOnly')).toBeFalsy();
+ $rootScope.isReadonly=true;
+ $rootScope.$digest();
+ expect(element.attr('readOnly')).toBeTruthy();
+ }));
+
+
+ it('should bind multiple', inject(function($rootScope, $compile) {
+ element = $compile('<select ng-multiple="isMultiple"></select>')($rootScope)
+ $rootScope.isMultiple=false;
+ $rootScope.$digest();
+ expect(element.attr('multiple')).toBeFalsy();
+ $rootScope.isMultiple='multiple';
+ $rootScope.$digest();
+ expect(element.attr('multiple')).toBeTruthy();
+ }));
+
+
+ it('should bind src', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-src="{{url}}" />')($rootScope)
+ $rootScope.url = 'http://localhost/';
+ $rootScope.$digest();
+ expect(element.attr('src')).toEqual('http://localhost/');
+ }));
+
+
+ it('should bind href and merge with other attrs', inject(function($rootScope, $compile) {
+ element = $compile('<a ng-href="{{url}}" rel="{{rel}}"></a>')($rootScope);
+ $rootScope.url = 'http://server';
+ $rootScope.rel = 'REL';
+ $rootScope.$digest();
+ expect(element.attr('href')).toEqual('http://server');
+ expect(element.attr('rel')).toEqual('REL');
+ }));
+});
+
+
+describe('ng-src', function() {
+
+ it('should interpolate the expression and bind to src', inject(function($compile, $rootScope) {
+ var element = $compile('<div ng-src="some/{{id}}"></div>')($rootScope)
+ $rootScope.$digest();
+ expect(element.attr('src')).toEqual('some/');
+
+ $rootScope.$apply(function() {
+ $rootScope.id = 1;
+ });
+ expect(element.attr('src')).toEqual('some/1');
+
+ dealoc(element);
+ }));
+});
+
+
+describe('ng-href', function() {
+
+ it('should interpolate the expression and bind to href', inject(function($compile, $rootScope) {
+ var element = $compile('<div ng-href="some/{{id}}"></div>')($rootScope)
+ $rootScope.$digest();
+ expect(element.attr('href')).toEqual('some/');
+
+ $rootScope.$apply(function() {
+ $rootScope.id = 1;
+ });
+ expect(element.attr('href')).toEqual('some/1');
+
+ dealoc(element);
+ }));
+});
diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js
new file mode 100644
index 00000000..5c34b5ad
--- /dev/null
+++ b/test/ng/directive/formSpec.js
@@ -0,0 +1,294 @@
+'use strict';
+
+describe('form', function() {
+ var doc, control, scope, $compile;
+
+ beforeEach(module(function($compileProvider) {
+ $compileProvider.directive('storeModelCtrl', function() {
+ return {
+ require: 'ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ control = ctrl;
+ }
+ };
+ });
+ }));
+
+ beforeEach(inject(function($injector) {
+ $compile = $injector.get('$compile');
+ scope = $injector.get('$rootScope');
+ }));
+
+ afterEach(function() {
+ dealoc(doc);
+ });
+
+
+ it('should instantiate form and attach it to DOM', function() {
+ doc = $compile('<form>')(scope);
+ expect(doc.data('$formController')).toBeTruthy();
+ expect(doc.data('$formController') instanceof FormController).toBe(true);
+ });
+
+
+ it('should remove the widget when element removed', function() {
+ doc = $compile(
+ '<form name="myForm">' +
+ '<input type="text" name="alias" ng-model="value" store-model-ctrl/>' +
+ '</form>')(scope);
+
+ var form = scope.myForm;
+ control.$setValidity('required', false);
+ expect(form.alias).toBe(control);
+ expect(form.$error.required).toEqual([control]);
+
+ doc.find('input').remove();
+ expect(form.$error.required).toBe(false);
+ expect(form.alias).toBeUndefined();
+ });
+
+
+ it('should use ng-form as form name', function() {
+ doc = $compile(
+ '<div ng-form="myForm">' +
+ '<input type="text" name="alias" ng-model="value"/>' +
+ '</div>')(scope);
+
+ expect(scope.myForm).toBeDefined();
+ expect(scope.myForm.alias).toBeDefined();
+ });
+
+
+ it('should prevent form submission', function() {
+ var startingUrl = '' + window.location;
+ doc = jqLite('<form name="myForm"><input type="submit" value="submit" />');
+ $compile(doc)(scope);
+
+ browserTrigger(doc.find('input'));
+ waitsFor(
+ function() { return true; },
+ 'let browser breath, so that the form submision can manifest itself', 10);
+
+ runs(function() {
+ expect('' + window.location).toEqual(startingUrl);
+ });
+ });
+
+
+ it('should not prevent form submission if action attribute present', function() {
+ var callback = jasmine.createSpy('submit').andCallFake(function(event) {
+ expect(event.isDefaultPrevented()).toBe(false);
+ event.preventDefault();
+ });
+
+ doc = $compile('<form name="x" action="some.py" />')(scope);
+ doc.bind('submit', callback);
+
+ browserTrigger(doc, 'submit');
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should publish form to scope when name attr is defined', function() {
+ doc = $compile('<form name="myForm"></form>')(scope);
+ expect(scope.myForm).toBeTruthy();
+ expect(doc.data('$formController')).toBeTruthy();
+ expect(doc.data('$formController')).toEqual(scope.myForm);
+ });
+
+
+ it('should allow form name to be an expression', function() {
+ doc = $compile('<form name="obj.myForm"></form>')(scope);
+
+ expect(scope['obj.myForm']).toBeTruthy();
+ });
+
+
+ it('should support two forms on a single scope', function() {
+ doc = $compile(
+ '<div>' +
+ '<form name="formA">' +
+ '<input name="firstName" ng-model="firstName" required>' +
+ '</form>' +
+ '<form name="formB">' +
+ '<input name="lastName" ng-model="lastName" required>' +
+ '</form>' +
+ '</div>'
+ )(scope);
+
+ scope.$apply();
+
+ expect(scope.formA.$error.required.length).toBe(1);
+ expect(scope.formA.$error.required).toEqual([scope.formA.firstName]);
+ expect(scope.formB.$error.required.length).toBe(1);
+ expect(scope.formB.$error.required).toEqual([scope.formB.lastName]);
+
+ var inputA = doc.find('input').eq(0),
+ inputB = doc.find('input').eq(1);
+
+ inputA.val('val1');
+ browserTrigger(inputA, 'blur');
+ inputB.val('val2');
+ browserTrigger(inputB, 'blur');
+
+ expect(scope.firstName).toBe('val1');
+ expect(scope.lastName).toBe('val2');
+
+ expect(scope.formA.$error.required).toBe(false);
+ expect(scope.formB.$error.required).toBe(false);
+ });
+
+
+ it('should publish widgets', function() {
+ doc = jqLite('<form name="form"><input type="text" name="w1" ng-model="some" /></form>');
+ $compile(doc)(scope);
+
+ var widget = scope.form.w1;
+ expect(widget).toBeDefined();
+ expect(widget.$pristine).toBe(true);
+ expect(widget.$dirty).toBe(false);
+ expect(widget.$valid).toBe(true);
+ expect(widget.$invalid).toBe(false);
+ });
+
+
+ describe('nested forms', function() {
+
+ it('should chain nested forms', function() {
+ doc = jqLite(
+ '<ng:form name="parent">' +
+ '<ng:form name="child">' +
+ '<input ng:model="modelA" name="inputA">' +
+ '<input ng:model="modelB" name="inputB">' +
+ '</ng:form>' +
+ '</ng:form>');
+ $compile(doc)(scope);
+
+ var parent = scope.parent,
+ child = scope.child,
+ inputA = child.inputA,
+ inputB = child.inputB;
+
+ inputA.$setValidity('MyError', false);
+ inputB.$setValidity('MyError', false);
+ expect(parent.$error.MyError).toEqual([child]);
+ expect(child.$error.MyError).toEqual([inputA, inputB]);
+
+ inputA.$setValidity('MyError', true);
+ expect(parent.$error.MyError).toEqual([child]);
+ expect(child.$error.MyError).toEqual([inputB]);
+
+ inputB.$setValidity('MyError', true);
+ expect(parent.$error.MyError).toBe(false);
+ expect(child.$error.MyError).toBe(false);
+ });
+
+
+ it('should deregister a child form when its DOM is removed', function() {
+ doc = jqLite(
+ '<form name="parent">' +
+ '<div class="ng-form" name="child">' +
+ '<input ng:model="modelA" name="inputA" required>' +
+ '</div>' +
+ '</form>');
+ $compile(doc)(scope);
+ scope.$apply();
+
+ var parent = scope.parent,
+ child = scope.child;
+
+ expect(parent).toBeDefined();
+ expect(child).toBeDefined();
+ expect(parent.$error.required).toEqual([child]);
+ doc.children().remove(); //remove child
+
+ expect(parent.child).toBeUndefined();
+ expect(scope.child).toBeUndefined();
+ expect(parent.$error.required).toBe(false);
+ });
+
+
+ it('should chain nested forms in repeater', function() {
+ doc = jqLite(
+ '<ng:form name=parent>' +
+ '<ng:form ng:repeat="f in forms" name=child>' +
+ '<input type=text ng:model=text name=text>' +
+ '</ng:form>' +
+ '</ng:form>');
+ $compile(doc)(scope);
+
+ scope.$apply(function() {
+ scope.forms = [1];
+ });
+
+ var parent = scope.parent;
+ var child = doc.find('input').scope().child;
+ var input = child.text;
+
+ expect(parent).toBeDefined();
+ expect(child).toBeDefined();
+ expect(input).toBeDefined();
+
+ input.$setValidity('myRule', false);
+ expect(input.$error.myRule).toEqual(true);
+ expect(child.$error.myRule).toEqual([input]);
+ expect(parent.$error.myRule).toEqual([child]);
+
+ input.$setValidity('myRule', true);
+ expect(parent.$error.myRule).toBe(false);
+ expect(child.$error.myRule).toBe(false);
+ });
+ })
+
+
+ describe('validation', function() {
+
+ beforeEach(function() {
+ doc = $compile(
+ '<form name="form">' +
+ '<input ng-model="name" name="name" store-model-ctrl/>' +
+ '</form>')(scope);
+
+ scope.$digest();
+ });
+
+
+ it('should have ng-valid/ng-invalid css class', function() {
+ expect(doc).toBeValid();
+
+ control.$setValidity('error', false);
+ expect(doc).toBeInvalid();
+ expect(doc.hasClass('ng-valid-error')).toBe(false);
+ expect(doc.hasClass('ng-invalid-error')).toBe(true);
+
+ control.$setValidity('another', false);
+ expect(doc.hasClass('ng-valid-error')).toBe(false);
+ expect(doc.hasClass('ng-invalid-error')).toBe(true);
+ expect(doc.hasClass('ng-valid-another')).toBe(false);
+ expect(doc.hasClass('ng-invalid-another')).toBe(true);
+
+ control.$setValidity('error', true);
+ expect(doc).toBeInvalid();
+ expect(doc.hasClass('ng-valid-error')).toBe(true);
+ expect(doc.hasClass('ng-invalid-error')).toBe(false);
+ expect(doc.hasClass('ng-valid-another')).toBe(false);
+ expect(doc.hasClass('ng-invalid-another')).toBe(true);
+
+ control.$setValidity('another', true);
+ expect(doc).toBeValid();
+ expect(doc.hasClass('ng-valid-error')).toBe(true);
+ expect(doc.hasClass('ng-invalid-error')).toBe(false);
+ expect(doc.hasClass('ng-valid-another')).toBe(true);
+ expect(doc.hasClass('ng-invalid-another')).toBe(false);
+ });
+
+
+ it('should have ng-pristine/ng-dirty css class', function() {
+ expect(doc).toBePristine();
+
+ control.$setViewValue('');
+ scope.$apply();
+ expect(doc).toBeDirty();
+ });
+ });
+});
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
new file mode 100644
index 00000000..e5f083b3
--- /dev/null
+++ b/test/ng/directive/inputSpec.js
@@ -0,0 +1,1119 @@
+'use strict';
+
+describe('NgModelController', function() {
+ var ctrl, scope, ngModelAccessor, element, parentFormCtrl;
+
+ beforeEach(inject(function($rootScope, $controller) {
+ var attrs = {name: 'testAlias'};
+
+ parentFormCtrl = {
+ $setValidity: jasmine.createSpy('$setValidity'),
+ $setDirty: jasmine.createSpy('$setDirty')
+ }
+
+ element = jqLite('<form><input></form>');
+ element.data('$formController', parentFormCtrl);
+
+ scope = $rootScope;
+ ngModelAccessor = jasmine.createSpy('ngModel accessor');
+ ctrl = $controller(NgModelController, {
+ $scope: scope, $element: element.find('input'), ngModel: ngModelAccessor, $attrs: attrs
+ });
+ // mock accessor (locals)
+ ngModelAccessor.andCallFake(function(val) {
+ if (isDefined(val)) scope.value = val;
+ return scope.value;
+ });
+ }));
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should init the properties', function() {
+ expect(ctrl.$dirty).toBe(false);
+ expect(ctrl.$pristine).toBe(true);
+ expect(ctrl.$valid).toBe(true);
+ expect(ctrl.$invalid).toBe(false);
+
+ expect(ctrl.$viewValue).toBeDefined();
+ expect(ctrl.$modelValue).toBeDefined();
+
+ expect(ctrl.$formatters).toEqual([]);
+ expect(ctrl.$parsers).toEqual([]);
+
+ expect(ctrl.$name).toBe('testAlias');
+ });
+
+
+ describe('setValidity', function() {
+
+ it('should propagate invalid to the parent form only when valid', function() {
+ expect(parentFormCtrl.$setValidity).not.toHaveBeenCalled();
+ ctrl.$setValidity('ERROR', false);
+ expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('ERROR', false, ctrl);
+
+ parentFormCtrl.$setValidity.reset();
+ ctrl.$setValidity('ERROR', false);
+ expect(parentFormCtrl.$setValidity).not.toHaveBeenCalled();
+ });
+
+
+ it('should set and unset the error', function() {
+ ctrl.$setValidity('required', false);
+ expect(ctrl.$error.required).toBe(true);
+
+ ctrl.$setValidity('required', true);
+ expect(ctrl.$error.required).toBe(false);
+ });
+
+
+ it('should set valid/invalid', function() {
+ ctrl.$setValidity('first', false);
+ expect(ctrl.$valid).toBe(false);
+ expect(ctrl.$invalid).toBe(true);
+
+ ctrl.$setValidity('second', false);
+ expect(ctrl.$valid).toBe(false);
+ expect(ctrl.$invalid).toBe(true);
+
+ ctrl.$setValidity('second', true);
+ expect(ctrl.$valid).toBe(false);
+ expect(ctrl.$invalid).toBe(true);
+
+ ctrl.$setValidity('first', true);
+ expect(ctrl.$valid).toBe(true);
+ expect(ctrl.$invalid).toBe(false);
+ });
+
+
+ it('should emit $valid only when $invalid', function() {
+ ctrl.$setValidity('error', true);
+ expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl);
+ parentFormCtrl.$setValidity.reset();
+
+ ctrl.$setValidity('error', false);
+ expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', false, ctrl);
+ parentFormCtrl.$setValidity.reset();
+ ctrl.$setValidity('error', true);
+ expect(parentFormCtrl.$setValidity).toHaveBeenCalledOnceWith('error', true, ctrl);
+ });
+ });
+
+
+ describe('view -> model', function() {
+
+ it('should set the value to $viewValue', function() {
+ ctrl.$setViewValue('some-val');
+ expect(ctrl.$viewValue).toBe('some-val');
+ });
+
+
+ it('should pipeline all registered parsers and set result to $modelValue', function() {
+ var log = [];
+
+ ctrl.$parsers.push(function(value) {
+ log.push(value);
+ return value + '-a';
+ });
+
+ ctrl.$parsers.push(function(value) {
+ log.push(value);
+ return value + '-b';
+ });
+
+ ctrl.$setViewValue('init');
+ expect(log).toEqual(['init', 'init-a']);
+ expect(ctrl.$modelValue).toBe('init-a-b');
+ });
+
+
+ it('should fire viewChangeListeners when the value changes in the view (even if invalid)',
+ function() {
+ var spy = jasmine.createSpy('viewChangeListener');
+ ctrl.$viewChangeListeners.push(spy);
+ ctrl.$setViewValue('val');
+ expect(spy).toHaveBeenCalledOnce();
+ spy.reset();
+
+ // invalid
+ ctrl.$parsers.push(function() {return undefined;});
+ ctrl.$setViewValue('val');
+ expect(spy).toHaveBeenCalledOnce();
+ });
+
+
+ it('should reset the model when the view is invalid', function() {
+ ctrl.$setViewValue('aaaa');
+ expect(ctrl.$modelValue).toBe('aaaa');
+
+ // add a validator that will make any input invalid
+ ctrl.$parsers.push(function() {return undefined;});
+ expect(ctrl.$modelValue).toBe('aaaa');
+ ctrl.$setViewValue('bbbb');
+ expect(ctrl.$modelValue).toBeUndefined();
+ });
+
+
+ it('should call parentForm.$setDirty only when pristine', function() {
+ ctrl.$setViewValue('');
+ expect(ctrl.$pristine).toBe(false);
+ expect(ctrl.$dirty).toBe(true);
+ expect(parentFormCtrl.$setDirty).toHaveBeenCalledOnce();
+
+ parentFormCtrl.$setDirty.reset();
+ ctrl.$setViewValue('');
+ expect(ctrl.$pristine).toBe(false);
+ expect(ctrl.$dirty).toBe(true);
+ expect(parentFormCtrl.$setDirty).not.toHaveBeenCalled();
+ });
+ });
+
+
+ describe('model -> view', function() {
+
+ it('should set the value to $modelValue', function() {
+ scope.$apply(function() {
+ scope.value = 10;
+ });
+ expect(ctrl.$modelValue).toBe(10);
+ });
+
+
+ it('should pipeline all registered formatters in reversed order and set result to $viewValue',
+ function() {
+ var log = [];
+
+ ctrl.$formatters.unshift(function(value) {
+ log.push(value);
+ return value + 2;
+ });
+
+ ctrl.$formatters.unshift(function(value) {
+ log.push(value);
+ return value + '';
+ });
+
+ scope.$apply(function() {
+ scope.value = 3;
+ });
+ expect(log).toEqual([3, 5]);
+ expect(ctrl.$viewValue).toBe('5');
+ });
+
+
+ it('should $render only if value changed', function() {
+ spyOn(ctrl, '$render');
+
+ scope.$apply(function() {
+ scope.value = 3;
+ });
+ expect(ctrl.$render).toHaveBeenCalledOnce();
+ ctrl.$render.reset();
+
+ ctrl.$formatters.push(function() {return 3;});
+ scope.$apply(function() {
+ scope.value = 5;
+ });
+ expect(ctrl.$render).not.toHaveBeenCalled();
+ });
+
+
+ it('should clear the view even if invalid', function() {
+ spyOn(ctrl, '$render');
+
+ ctrl.$formatters.push(function() {return undefined;});
+ scope.$apply(function() {
+ scope.value = 5;
+ });
+ expect(ctrl.$render).toHaveBeenCalledOnce();
+ });
+ });
+});
+
+describe('ng-model', function() {
+
+ it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty)',
+ inject(function($compile, $rootScope) {
+ var element = $compile('<input type="email" ng-model="value" />')($rootScope);
+
+ $rootScope.$digest();
+ expect(element).toBeValid();
+ expect(element).toBePristine();
+ expect(element.hasClass('ng-valid-email')).toBe(true);
+ expect(element.hasClass('ng-invalid-email')).toBe(false);
+
+ $rootScope.$apply(function() {
+ $rootScope.value = 'invalid-email';
+ });
+ expect(element).toBeInvalid();
+ expect(element).toBePristine();
+ expect(element.hasClass('ng-valid-email')).toBe(false);
+ expect(element.hasClass('ng-invalid-email')).toBe(true);
+
+ element.val('invalid-again');
+ browserTrigger(element, 'blur');
+ expect(element).toBeInvalid();
+ expect(element).toBeDirty();
+ expect(element.hasClass('ng-valid-email')).toBe(false);
+ expect(element.hasClass('ng-invalid-email')).toBe(true);
+
+ element.val('vojta@google.com');
+ browserTrigger(element, 'blur');
+ expect(element).toBeValid();
+ expect(element).toBeDirty();
+ expect(element.hasClass('ng-valid-email')).toBe(true);
+ expect(element.hasClass('ng-invalid-email')).toBe(false);
+
+ dealoc(element);
+ }));
+
+
+ it('should set invalid classes on init', inject(function($compile, $rootScope) {
+ var element = $compile('<input type="email" ng-model="value" required />')($rootScope);
+ $rootScope.$digest();
+
+ expect(element).toBeInvalid();
+ expect(element).toHaveClass('ng-invalid-required');
+ }));
+});
+
+
+describe('input', function() {
+ var formElm, inputElm, scope, $compile;
+
+ function compileInput(inputHtml) {
+ formElm = jqLite('<form name="form">' + inputHtml + '</form>');
+ inputElm = formElm.find('input');
+ $compile(formElm)(scope);
+ }
+
+ function changeInputValueTo(value) {
+ inputElm.val(value);
+ browserTrigger(inputElm, 'blur');
+ }
+
+ beforeEach(inject(function($injector) {
+ $compile = $injector.get('$compile');
+ scope = $injector.get('$rootScope');
+ }));
+
+ afterEach(function() {
+ dealoc(formElm);
+ });
+
+
+ it('should bind to a model', function() {
+ compileInput('<input type="text" ng-model="name" name="alias" ng-change="change()" />');
+
+ scope.$apply(function() {
+ scope.name = 'misko';
+ });
+
+ expect(inputElm.val()).toBe('misko');
+ });
+
+
+ it('should not set readonly or disabled property on ie7', function() {
+ this.addMatchers({
+ toBeOff: function(attributeName) {
+ var actualValue = this.actual.attr(attributeName);
+ this.message = function() {
+ return "Attribute '" + attributeName + "' expected to be off but was '" + actualValue +
+ "' in: " + angular.mock.dump(this.actual);
+ }
+
+ return !actualValue || actualValue == 'false';
+ }
+ });
+
+ compileInput('<input type="text" ng-model="name" name="alias"/>');
+ expect(inputElm.prop('readOnly')).toBe(false);
+ expect(inputElm.prop('disabled')).toBe(false);
+
+ expect(inputElm).toBeOff('readOnly');
+ expect(inputElm).toBeOff('readonly');
+ expect(inputElm).toBeOff('disabled');
+ });
+
+
+ it('should cleanup it self from the parent form', function() {
+ compileInput('<input ng-model="name" name="alias" required>');
+
+ scope.$apply();
+ expect(scope.form.$error.required.length).toBe(1);
+
+ inputElm.remove();
+ expect(scope.form.$error.required).toBe(false);
+ });
+
+
+ it('should update the model on "blur" event', function() {
+ compileInput('<input type="text" ng-model="name" name="alias" ng-change="change()" />');
+
+ changeInputValueTo('adam');
+ expect(scope.name).toEqual('adam');
+ });
+
+
+ it('should update the model and trim the value', function() {
+ compileInput('<input type="text" ng-model="name" name="alias" ng-change="change()" />');
+
+ changeInputValueTo(' a ');
+ expect(scope.name).toEqual('a');
+ });
+
+
+ it('should allow complex reference binding', function() {
+ compileInput('<input type="text" ng-model="obj[\'abc\'].name"/>');
+
+ scope.$apply(function() {
+ scope.obj = { abc: { name: 'Misko'} };
+ });
+ expect(inputElm.val()).toEqual('Misko');
+ });
+
+
+ it('should ignore input without ng-model attr', function() {
+ compileInput('<input type="text" name="whatever" required />');
+
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.hasClass('ng-valid')).toBe(false);
+ expect(inputElm.hasClass('ng-invalid')).toBe(false);
+ expect(inputElm.hasClass('ng-pristine')).toBe(false);
+ expect(inputElm.hasClass('ng-dirty')).toBe(false);
+ });
+
+
+ it('should report error on assignment error', function() {
+ expect(function() {
+ compileInput('<input type="text" ng-model="throw \'\'">');
+ scope.$digest();
+ }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at [''].");
+ });
+
+
+ it("should render as blank if null", function() {
+ compileInput('<input type="text" ng-model="age" />');
+
+ scope.$apply(function() {
+ scope.age = null;
+ });
+
+ expect(scope.age).toBeNull();
+ expect(inputElm.val()).toEqual('');
+ });
+
+
+ it('should render 0 even if it is a number', function() {
+ compileInput('<input type="text" ng-model="value" />');
+ scope.$apply(function() {
+ scope.value = 0;
+ });
+
+ expect(inputElm.val()).toBe('0');
+ });
+
+
+ describe('pattern', function() {
+
+ it('should validate in-lined pattern', function() {
+ compileInput('<input type="text" ng-model="value" ng-pattern="/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/" />');
+ scope.$digest();
+
+ changeInputValueTo('x000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('000-00-0000');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('123-45-6789');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('x');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should validate pattern from scope', function() {
+ compileInput('<input type="text" ng-model="value" ng-pattern="regexp" />');
+ scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/;
+ scope.$digest();
+
+ changeInputValueTo('x000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('000-00-0000');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('123-45-6789');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('x');
+ expect(inputElm).toBeInvalid();
+
+ scope.regexp = /abc?/;
+
+ changeInputValueTo('ab');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('xx');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ xit('should throw an error when scope pattern can\'t be found', function() {
+ compileInput('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
+
+ expect(function() { changeInputValueTo('xx'); }).
+ toThrow('Expected fooRegexp to be a RegExp but was undefined');
+ });
+ });
+
+
+ describe('minlength', function() {
+
+ it('should invalid shorter than given minlenght', function() {
+ compileInput('<input type="text" ng-model="value" ng-minlength="3" />');
+
+ changeInputValueTo('aa');
+ expect(scope.value).toBeUndefined();
+
+ changeInputValueTo('aaa');
+ expect(scope.value).toBe('aaa');
+ });
+ });
+
+
+ describe('maxlength', function() {
+
+ it('should invalid shorter than given maxlenght', function() {
+ compileInput('<input type="text" ng-model="value" ng-maxlength="5" />');
+
+ changeInputValueTo('aaaaaaaa');
+ expect(scope.value).toBeUndefined();
+
+ changeInputValueTo('aaa');
+ expect(scope.value).toBe('aaa');
+ });
+ });
+
+
+ // INPUT TYPES
+
+ describe('number', function() {
+
+ it('should reset the model if view is invalid', function() {
+ compileInput('<input type="number" ng-model="age"/>');
+
+ scope.$apply(function() {
+ scope.age = 123;
+ });
+ expect(inputElm.val()).toBe('123');
+
+ try {
+ // to allow non-number values, we have to change type so that
+ // the browser which have number validation will not interfere with
+ // this test. IE8 won't allow it hence the catch.
+ inputElm[0].setAttribute('type', 'text');
+ } catch (e) {}
+
+ changeInputValueTo('123X');
+ expect(inputElm.val()).toBe('123X');
+ expect(scope.age).toBeUndefined();
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should render as blank if null', function() {
+ compileInput('<input type="number" ng-model="age" />');
+
+ scope.$apply(function() {
+ scope.age = null;
+ });
+
+ expect(scope.age).toBeNull();
+ expect(inputElm.val()).toEqual('');
+ });
+
+
+ it('should come up blank when no value specified', function() {
+ compileInput('<input type="number" ng-model="age" />');
+
+ scope.$digest();
+ expect(inputElm.val()).toBe('');
+
+ scope.$apply(function() {
+ scope.age = null;
+ });
+
+ expect(scope.age).toBeNull();
+ expect(inputElm.val()).toBe('');
+ });
+
+
+ it('should parse empty string to null', function() {
+ compileInput('<input type="number" ng-model="age" />');
+
+ scope.$apply(function() {
+ scope.age = 10;
+ });
+
+ changeInputValueTo('');
+ expect(scope.age).toBeNull();
+ expect(inputElm).toBeValid();
+ });
+
+
+ describe('min', function() {
+
+ it('should validate', function() {
+ compileInput('<input type="number" ng-model="value" name="alias" min="10" />');
+ scope.$digest();
+
+ changeInputValueTo('1');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.$error.min).toBeTruthy();
+
+ changeInputValueTo('100');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(100);
+ expect(scope.form.alias.$error.min).toBeFalsy();
+ });
+ });
+
+
+ describe('max', function() {
+
+ it('should validate', function() {
+ compileInput('<input type="number" ng-model="value" name="alias" max="10" />');
+ scope.$digest();
+
+ changeInputValueTo('20');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.$error.max).toBeTruthy();
+
+ changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.$error.max).toBeFalsy();
+ });
+ });
+
+
+ describe('required', function() {
+
+ it('should be valid even if value is 0', function() {
+ compileInput('<input type="number" ng-model="value" name="alias" required />');
+
+ changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.$error.required).toBeFalsy();
+ });
+
+ it('should be valid even if value 0 is set from model', function() {
+ compileInput('<input type="number" ng-model="value" name="alias" required />');
+
+ scope.$apply(function() {
+ scope.value = 0;
+ });
+
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('0')
+ expect(scope.form.alias.$error.required).toBeFalsy();
+ });
+ });
+ });
+
+ describe('email', function() {
+
+ it('should validate e-mail', function() {
+ compileInput('<input type="email" ng-model="email" name="alias" />');
+
+ var widget = scope.form.alias;
+ changeInputValueTo('vojta@google.com');
+
+ expect(scope.email).toBe('vojta@google.com');
+ expect(inputElm).toBeValid();
+ expect(widget.$error.email).toBe(false);
+
+ changeInputValueTo('invalid@');
+ expect(scope.email).toBeUndefined();
+ expect(inputElm).toBeInvalid();
+ expect(widget.$error.email).toBeTruthy();
+ });
+
+
+ describe('EMAIL_REGEXP', function() {
+
+ it('should validate email', function() {
+ expect(EMAIL_REGEXP.test('a@b.com')).toBe(true);
+ expect(EMAIL_REGEXP.test('a@B.c')).toBe(false);
+ });
+ });
+ });
+
+
+ describe('url', function() {
+
+ it('should validate url', function() {
+ compileInput('<input type="url" ng-model="url" name="alias" />');
+ var widget = scope.form.alias;
+
+ changeInputValueTo('http://www.something.com');
+ expect(scope.url).toBe('http://www.something.com');
+ expect(inputElm).toBeValid();
+ expect(widget.$error.url).toBe(false);
+
+ changeInputValueTo('invalid.com');
+ expect(scope.url).toBeUndefined();
+ expect(inputElm).toBeInvalid();
+ expect(widget.$error.url).toBeTruthy();
+ });
+
+
+ describe('URL_REGEXP', function() {
+
+ it('should validate url', function() {
+ expect(URL_REGEXP.test('http://server:123/path')).toBe(true);
+ expect(URL_REGEXP.test('a@B.c')).toBe(false);
+ });
+ });
+ });
+
+
+ describe('radio', function() {
+
+ it('should update the model', function() {
+ compileInput(
+ '<input type="radio" ng-model="color" value="white" />' +
+ '<input type="radio" ng-model="color" value="red" />' +
+ '<input type="radio" ng-model="color" value="blue" />');
+
+ scope.$apply(function() {
+ scope.color = 'white';
+ });
+ expect(inputElm[0].checked).toBe(true);
+ expect(inputElm[1].checked).toBe(false);
+ expect(inputElm[2].checked).toBe(false);
+
+ scope.$apply(function() {
+ scope.color = 'red';
+ });
+ expect(inputElm[0].checked).toBe(false);
+ expect(inputElm[1].checked).toBe(true);
+ expect(inputElm[2].checked).toBe(false);
+
+ browserTrigger(inputElm[2]);
+ expect(scope.color).toBe('blue');
+ });
+
+
+ it('should allow {{expr}} as value', function() {
+ scope.some = 11;
+ compileInput(
+ '<input type="radio" ng-model="value" value="{{some}}" />' +
+ '<input type="radio" ng-model="value" value="{{other}}" />');
+
+ scope.$apply(function() {
+ scope.value = 'blue';
+ scope.some = 'blue';
+ scope.other = 'red';
+ });
+
+ expect(inputElm[0].checked).toBe(true);
+ expect(inputElm[1].checked).toBe(false);
+
+ browserTrigger(inputElm[1]);
+ expect(scope.value).toBe('red');
+
+ scope.$apply(function() {
+ scope.other = 'non-red';
+ });
+
+ expect(inputElm[0].checked).toBe(false);
+ expect(inputElm[1].checked).toBe(false);
+ });
+ });
+
+
+ describe('checkbox', function() {
+
+ it('should ignore checkbox without ng-model attr', function() {
+ compileInput('<input type="checkbox" name="whatever" required />');
+
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.hasClass('ng-valid')).toBe(false);
+ expect(inputElm.hasClass('ng-invalid')).toBe(false);
+ expect(inputElm.hasClass('ng-pristine')).toBe(false);
+ expect(inputElm.hasClass('ng-dirty')).toBe(false);
+ });
+
+
+ it('should format booleans', function() {
+ compileInput('<input type="checkbox" ng-model="name" />');
+
+ scope.$apply(function() {
+ scope.name = false;
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ scope.$apply(function() {
+ scope.name = true;
+ });
+ expect(inputElm[0].checked).toBe(true);
+ });
+
+
+ it('should support type="checkbox" with non-standard capitalization', function() {
+ compileInput('<input type="checkBox" ng-model="checkbox" />');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(true);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(false);
+ });
+
+
+ it('should allow custom enumeration', function() {
+ compileInput('<input type="checkbox" ng-model="name" ng-true-value="y" ' +
+ 'ng-false-value="n">');
+
+ scope.$apply(function() {
+ scope.name = 'y';
+ });
+ expect(inputElm[0].checked).toBe(true);
+
+ scope.$apply(function() {
+ scope.name = 'n';
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ scope.$apply(function() {
+ scope.name = 'something else';
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.name).toEqual('y');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.name).toEqual('n');
+ });
+
+
+ it('should be required if false', function() {
+ compileInput('<input type="checkbox" ng:model="value" required />');
+
+ browserTrigger(inputElm, 'click');
+ expect(inputElm[0].checked).toBe(true);
+ expect(inputElm).toBeValid();
+
+ browserTrigger(inputElm, 'click');
+ expect(inputElm[0].checked).toBe(false);
+ expect(inputElm).toBeInvalid();
+ });
+ });
+
+
+ describe('textarea', function() {
+
+ it("should process textarea", function() {
+ compileInput('<textarea ng-model="name"></textarea>');
+ inputElm = formElm.find('textarea');
+
+ scope.$apply(function() {
+ scope.name = 'Adam';
+ });
+ expect(inputElm.val()).toEqual('Adam');
+
+ changeInputValueTo('Shyam');
+ expect(scope.name).toEqual('Shyam');
+
+ changeInputValueTo('Kai');
+ expect(scope.name).toEqual('Kai');
+ });
+
+
+ it('should ignore textarea without ng-model attr', function() {
+ compileInput('<textarea name="whatever" required></textarea>');
+ inputElm = formElm.find('textarea');
+
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.hasClass('ng-valid')).toBe(false);
+ expect(inputElm.hasClass('ng-invalid')).toBe(false);
+ expect(inputElm.hasClass('ng-pristine')).toBe(false);
+ expect(inputElm.hasClass('ng-dirty')).toBe(false);
+ });
+ });
+
+
+ describe('ng-list', function() {
+
+ it('should parse text into an array', function() {
+ compileInput('<input type="text" ng-model="list" ng-list />');
+
+ // model -> view
+ scope.$apply(function() {
+ scope.list = ['x', 'y', 'z'];
+ });
+ expect(inputElm.val()).toBe('x, y, z');
+
+ // view -> model
+ changeInputValueTo('1, 2, 3');
+ expect(scope.list).toEqual(['1', '2', '3']);
+ });
+
+
+ it("should not clobber text if model changes due to itself", function() {
+ // When the user types 'a,b' the 'a,' stage parses to ['a'] but if the
+ // $parseModel function runs it will change to 'a', in essence preventing
+ // the user from ever typying ','.
+ compileInput('<input type="text" ng-model="list" ng-list />');
+
+ changeInputValueTo('a ');
+ expect(inputElm.val()).toEqual('a ');
+ expect(scope.list).toEqual(['a']);
+
+ changeInputValueTo('a ,');
+ expect(inputElm.val()).toEqual('a ,');
+ expect(scope.list).toEqual(['a']);
+
+ changeInputValueTo('a , ');
+ expect(inputElm.val()).toEqual('a , ');
+ expect(scope.list).toEqual(['a']);
+
+ changeInputValueTo('a , b');
+ expect(inputElm.val()).toEqual('a , b');
+ expect(scope.list).toEqual(['a', 'b']);
+ });
+
+
+ xit('should require at least one item', function() {
+ compileInput('<input type="text" ng-model="list" ng-list required />');
+
+ changeInputValueTo(' , ');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should convert empty string to an empty array', function() {
+ compileInput('<input type="text" ng-model="list" ng-list />');
+
+ changeInputValueTo('');
+ expect(scope.list).toEqual([]);
+ });
+
+
+ it('should allow custom separator', function() {
+ compileInput('<input type="text" ng-model="list" ng-list=":" />');
+
+ changeInputValueTo('a,a');
+ expect(scope.list).toEqual(['a,a']);
+
+ changeInputValueTo('a:b');
+ expect(scope.list).toEqual(['a', 'b']);
+ });
+
+
+ it('should allow regexp as a separator', function() {
+ compileInput('<input type="text" ng-model="list" ng-list="/:|,/" />');
+
+ changeInputValueTo('a,b');
+ expect(scope.list).toEqual(['a', 'b']);
+
+ changeInputValueTo('a,b: c');
+ expect(scope.list).toEqual(['a', 'b', 'c']);
+ });
+ });
+
+ describe('required', function() {
+
+ it('should allow bindings on ng-required', function() {
+ compileInput('<input type="text" ng-model="value" ng-required="required" />');
+
+ scope.$apply(function() {
+ scope.required = false;
+ });
+
+ changeInputValueTo('');
+ expect(inputElm).toBeValid();
+
+
+ scope.$apply(function() {
+ scope.required = true;
+ });
+ expect(inputElm).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.value = 'some';
+ });
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('');
+ expect(inputElm).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.required = false;
+ });
+ expect(inputElm).toBeValid();
+ });
+
+
+ it('should invalid initial value with bound required', function() {
+ compileInput('<input type="text" ng-model="value" required="{{required}}" />');
+
+ scope.$apply(function() {
+ scope.required = true;
+ });
+
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should be $invalid but $pristine if not touched', function() {
+ compileInput('<input type="text" ng-model="name" name="alias" required />');
+
+ scope.$apply(function() {
+ scope.name = '';
+ });
+
+ expect(inputElm).toBeInvalid();
+ expect(inputElm).toBePristine();
+
+ changeInputValueTo('');
+ expect(inputElm).toBeInvalid();
+ expect(inputElm).toBeDirty();
+ });
+
+
+ it('should allow empty string if not required', function() {
+ compileInput('<input type="text" ng-model="foo" />');
+ changeInputValueTo('a');
+ changeInputValueTo('');
+ expect(scope.foo).toBe('');
+ });
+
+
+ it('should set $invalid when model undefined', function() {
+ compileInput('<input type="text" ng-model="notDefiend" required />');
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+ })
+ });
+
+
+ describe('ng-change', function() {
+
+ it('should $eval expression after new value is set in the model', function() {
+ compileInput('<input type="text" ng-model="value" ng-change="change()" />');
+
+ scope.change = jasmine.createSpy('change').andCallFake(function() {
+ expect(scope.value).toBe('new value');
+ });
+
+ changeInputValueTo('new value');
+ expect(scope.change).toHaveBeenCalledOnce();
+ });
+
+ it('should not $eval the expression if changed from model', function() {
+ compileInput('<input type="text" ng-model="value" ng-change="change()" />');
+
+ scope.change = jasmine.createSpy('change');
+ scope.$apply(function() {
+ scope.value = true;
+ });
+
+ expect(scope.change).not.toHaveBeenCalled();
+ });
+
+
+ it('should $eval ng-change expression on checkbox', function() {
+ compileInput('<input type="checkbox" ng-model="foo" ng-change="changeFn()">');
+
+ scope.changeFn = jasmine.createSpy('changeFn');
+ scope.$digest();
+ expect(scope.changeFn).not.toHaveBeenCalled();
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.changeFn).toHaveBeenCalledOnce();
+ });
+ });
+
+
+ describe('ng-model-instant', function() {
+
+ it('should bind keydown, change, input events', inject(function($browser) {
+ compileInput('<input type="text" ng-model="value" ng-model-instant />');
+
+ inputElm.val('value1');
+ browserTrigger(inputElm, 'keydown');
+
+ // should be async (because of keydown)
+ expect(scope.value).toBeUndefined();
+
+ $browser.defer.flush();
+ expect(scope.value).toBe('value1');
+
+ inputElm.val('value2');
+ browserTrigger(inputElm, 'change');
+ expect(scope.value).toBe('value2');
+
+ if (msie < 9) return;
+
+ inputElm.val('value3');
+ browserTrigger(inputElm, 'input');
+ expect(scope.value).toBe('value3');
+ }));
+ });
+
+
+ describe('ng-value', function() {
+
+ it('should evaluate and set constant expressions', function() {
+ compileInput('<input type="radio" ng-model="selected" ng-value="true">' +
+ '<input type="radio" ng-model="selected" ng-value="false">' +
+ '<input type="radio" ng-model="selected" ng-value="1">');
+ scope.$digest();
+
+ browserTrigger(inputElm[0], 'click');
+ expect(scope.selected).toBe(true);
+
+ browserTrigger(inputElm[1], 'click');
+ expect(scope.selected).toBe(false);
+
+ browserTrigger(inputElm[2], 'click');
+ expect(scope.selected).toBe(1);
+ });
+
+
+ it('should watch the expression', function() {
+ compileInput('<input type="radio" ng-model="selected" ng-value="value">');
+
+ scope.$apply(function() {
+ scope.selected = scope.value = {some: 'object'};
+ });
+ expect(inputElm[0].checked).toBe(true);
+
+ scope.$apply(function() {
+ scope.value = {some: 'other'};
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.selected).toBe(scope.value);
+ });
+ });
+});
diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js
new file mode 100644
index 00000000..01a07c52
--- /dev/null
+++ b/test/ng/directive/ngBindSpec.js
@@ -0,0 +1,80 @@
+'use strict';
+
+describe('ng-bind-*', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ describe('ng-bind', function() {
+
+ it('should set text', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-bind="a"></div>')($rootScope);
+ expect(element.text()).toEqual('');
+ $rootScope.a = 'misko';
+ $rootScope.$digest();
+ expect(element.hasClass('ng-binding')).toEqual(true);
+ expect(element.text()).toEqual('misko');
+ }));
+
+ it('should set text to blank if undefined', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-bind="a"></div>')($rootScope);
+ $rootScope.a = 'misko';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko');
+ $rootScope.a = undefined;
+ $rootScope.$digest();
+ expect(element.text()).toEqual('');
+ $rootScope.a = null;
+ $rootScope.$digest();
+ expect(element.text()).toEqual('');
+ }));
+
+ it('should set html', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-bind-html="html"></div>')($rootScope);
+ $rootScope.html = '<div unknown>hello</div>';
+ $rootScope.$digest();
+ expect(lowercase(element.html())).toEqual('<div>hello</div>');
+ }));
+
+ it('should set unsafe html', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-bind-html-unsafe="html"></div>')($rootScope);
+ $rootScope.html = '<div onclick="">hello</div>';
+ $rootScope.$digest();
+ expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
+ }));
+
+ it('should suppress rendering of falsy values', inject(function($rootScope, $compile) {
+ element = $compile('<div>{{ null }}{{ undefined }}{{ "" }}-{{ 0 }}{{ false }}</div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('-0false');
+ }));
+
+ it('should render object as JSON ignore $$', inject(function($rootScope, $compile) {
+ element = $compile('<div>{{ {key:"value", $$key:"hide"} }}</div>')($rootScope);
+ $rootScope.$digest();
+ expect(fromJson(element.text())).toEqual({key:'value'});
+ }));
+ });
+
+
+ describe('ng-bind-template', function() {
+
+ it('should ng-bind-template', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-bind-template="Hello {{name}}!"></div>')($rootScope);
+ $rootScope.name = 'Misko';
+ $rootScope.$digest();
+ expect(element.hasClass('ng-binding')).toEqual(true);
+ expect(element.text()).toEqual('Hello Misko!');
+ }));
+
+ it('should render object as JSON ignore $$', inject(function($rootScope, $compile) {
+ element = $compile('<pre>{{ {key:"value", $$key:"hide"} }}</pre>')($rootScope);
+ $rootScope.$digest();
+ expect(fromJson(element.text())).toEqual({key:'value'});
+ }));
+ });
+});
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
new file mode 100644
index 00000000..2297e343
--- /dev/null
+++ b/test/ng/directive/ngClassSpec.js
@@ -0,0 +1,204 @@
+'use strict';
+
+describe('ng-class', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should add new and remove old classes dynamically', inject(function($rootScope, $compile) {
+ element = $compile('<div class="existing" ng-class="dynClass"></div>')($rootScope);
+ $rootScope.dynClass = 'A';
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('A')).toBe(true);
+
+ $rootScope.dynClass = 'B';
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('A')).toBe(false);
+ expect(element.hasClass('B')).toBe(true);
+
+ delete $rootScope.dynClass;
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('A')).toBe(false);
+ expect(element.hasClass('B')).toBe(false);
+ }));
+
+
+ it('should support adding multiple classes via an array', inject(function($rootScope, $compile) {
+ element = $compile('<div class="existing" ng-class="[\'A\', \'B\']"></div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBeTruthy();
+ expect(element.hasClass('A')).toBeTruthy();
+ expect(element.hasClass('B')).toBeTruthy();
+ }));
+
+
+ it('should support adding multiple classes conditionally via a map of class names to boolean' +
+ 'expressions', inject(function($rootScope, $compile) {
+ var element = $compile(
+ '<div class="existing" ' +
+ 'ng-class="{A: conditionA, B: conditionB(), AnotB: conditionA&&!conditionB}">' +
+ '</div>')($rootScope);
+ $rootScope.conditionA = true;
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBeTruthy();
+ expect(element.hasClass('A')).toBeTruthy();
+ expect(element.hasClass('B')).toBeFalsy();
+ expect(element.hasClass('AnotB')).toBeTruthy();
+
+ $rootScope.conditionB = function() { return true };
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBeTruthy();
+ expect(element.hasClass('A')).toBeTruthy();
+ expect(element.hasClass('B')).toBeTruthy();
+ expect(element.hasClass('AnotB')).toBeFalsy();
+ }));
+
+
+ it('should support adding multiple classes via a space delimited string', inject(function($rootScope, $compile) {
+ element = $compile('<div class="existing" ng-class="\'A B\'"></div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBeTruthy();
+ expect(element.hasClass('A')).toBeTruthy();
+ expect(element.hasClass('B')).toBeTruthy();
+ }));
+
+
+ it('should preserve class added post compilation with pre-existing classes', inject(function($rootScope, $compile) {
+ element = $compile('<div class="existing" ng-class="dynClass"></div>')($rootScope);
+ $rootScope.dynClass = 'A';
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+
+ // add extra class, change model and eval
+ element.addClass('newClass');
+ $rootScope.dynClass = 'B';
+ $rootScope.$digest();
+
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('B')).toBe(true);
+ expect(element.hasClass('newClass')).toBe(true);
+ }));
+
+
+ it('should preserve class added post compilation without pre-existing classes"', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-class="dynClass"></div>')($rootScope);
+ $rootScope.dynClass = 'A';
+ $rootScope.$digest();
+ expect(element.hasClass('A')).toBe(true);
+
+ // add extra class, change model and eval
+ element.addClass('newClass');
+ $rootScope.dynClass = 'B';
+ $rootScope.$digest();
+
+ expect(element.hasClass('B')).toBe(true);
+ expect(element.hasClass('newClass')).toBe(true);
+ }));
+
+
+ it('should preserve other classes with similar name"', inject(function($rootScope, $compile) {
+ element = $compile('<div class="ui-panel ui-selected" ng-class="dynCls"></div>')($rootScope);
+ $rootScope.dynCls = 'panel';
+ $rootScope.$digest();
+ $rootScope.dynCls = 'foo';
+ $rootScope.$digest();
+ expect(element[0].className).toBe('ui-panel ui-selected ng-scope foo');
+ }));
+
+
+ it('should not add duplicate classes', inject(function($rootScope, $compile) {
+ element = $compile('<div class="panel bar" ng-class="dynCls"></div>')($rootScope);
+ $rootScope.dynCls = 'panel';
+ $rootScope.$digest();
+ expect(element[0].className).toBe('panel bar ng-scope');
+ }));
+
+
+ it('should remove classes even if it was specified via class attribute', inject(function($rootScope, $compile) {
+ element = $compile('<div class="panel bar" ng-class="dynCls"></div>')($rootScope);
+ $rootScope.dynCls = 'panel';
+ $rootScope.$digest();
+ $rootScope.dynCls = 'window';
+ $rootScope.$digest();
+ expect(element[0].className).toBe('bar ng-scope window');
+ }));
+
+
+ it('should remove classes even if they were added by another code', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-class="dynCls"></div>')($rootScope);
+ $rootScope.dynCls = 'foo';
+ $rootScope.$digest();
+ element.addClass('foo');
+ $rootScope.dynCls = '';
+ $rootScope.$digest();
+ }));
+
+
+ it('should convert undefined and null values to an empty string', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-class="dynCls"></div>')($rootScope);
+ $rootScope.dynCls = [undefined, null];
+ $rootScope.$digest();
+ }));
+
+
+ it('should ng-class odd/even', inject(function($rootScope, $compile) {
+ element = $compile('<ul><li ng-repeat="i in [0,1]" class="existing" ng-class-odd="\'odd\'" ng-class-even="\'even\'"></li><ul>')($rootScope);
+ $rootScope.$digest();
+ var e1 = jqLite(element[0].childNodes[1]);
+ var e2 = jqLite(element[0].childNodes[2]);
+ expect(e1.hasClass('existing')).toBeTruthy();
+ expect(e1.hasClass('odd')).toBeTruthy();
+ expect(e2.hasClass('existing')).toBeTruthy();
+ expect(e2.hasClass('even')).toBeTruthy();
+ }));
+
+
+ it('should allow both ng-class and ng-class-odd/even on the same element', inject(function($rootScope, $compile) {
+ element = $compile('<ul>' +
+ '<li ng-repeat="i in [0,1]" ng-class="\'plainClass\'" ' +
+ 'ng-class-odd="\'odd\'" ng-class-even="\'even\'"></li>' +
+ '<ul>')($rootScope);
+ $rootScope.$apply();
+ var e1 = jqLite(element[0].childNodes[1]);
+ var e2 = jqLite(element[0].childNodes[2]);
+
+ expect(e1.hasClass('plainClass')).toBeTruthy();
+ expect(e1.hasClass('odd')).toBeTruthy();
+ expect(e1.hasClass('even')).toBeFalsy();
+ expect(e2.hasClass('plainClass')).toBeTruthy();
+ expect(e2.hasClass('even')).toBeTruthy();
+ expect(e2.hasClass('odd')).toBeFalsy();
+ }));
+
+
+ it('should allow both ng-class and ng-class-odd/even with multiple classes', inject(function($rootScope, $compile) {
+ element = $compile('<ul>' +
+ '<li ng-repeat="i in [0,1]" ng-class="[\'A\', \'B\']" ' +
+ 'ng-class-odd="[\'C\', \'D\']" ng-class-even="[\'E\', \'F\']"></li>' +
+ '<ul>')($rootScope);
+ $rootScope.$apply();
+ var e1 = jqLite(element[0].childNodes[1]);
+ var e2 = jqLite(element[0].childNodes[2]);
+
+ expect(e1.hasClass('A')).toBeTruthy();
+ expect(e1.hasClass('B')).toBeTruthy();
+ expect(e1.hasClass('C')).toBeTruthy();
+ expect(e1.hasClass('D')).toBeTruthy();
+ expect(e1.hasClass('E')).toBeFalsy();
+ expect(e1.hasClass('F')).toBeFalsy();
+
+ expect(e2.hasClass('A')).toBeTruthy();
+ expect(e2.hasClass('B')).toBeTruthy();
+ expect(e2.hasClass('E')).toBeTruthy();
+ expect(e2.hasClass('F')).toBeTruthy();
+ expect(e2.hasClass('C')).toBeFalsy();
+ expect(e2.hasClass('D')).toBeFalsy();
+ }));
+});
diff --git a/test/ng/directive/ngClickSpec.js b/test/ng/directive/ngClickSpec.js
new file mode 100644
index 00000000..f5086d1c
--- /dev/null
+++ b/test/ng/directive/ngClickSpec.js
@@ -0,0 +1,26 @@
+'use strict';
+
+describe('ng-click', function() {
+ var element;
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+ it('should get called on a click', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-click="clicked = true"></div>')($rootScope);
+ $rootScope.$digest();
+ expect($rootScope.clicked).toBeFalsy();
+
+ browserTrigger(element, 'click');
+ expect($rootScope.clicked).toEqual(true);
+ }));
+
+ it('should pass event object', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-click="event = $event"></div>')($rootScope);
+ $rootScope.$digest();
+
+ browserTrigger(element, 'click');
+ expect($rootScope.event).toBeDefined();
+ }));
+});
diff --git a/test/ng/directive/ngCloakSpec.js b/test/ng/directive/ngCloakSpec.js
new file mode 100644
index 00000000..f3c28b60
--- /dev/null
+++ b/test/ng/directive/ngCloakSpec.js
@@ -0,0 +1,49 @@
+'use strict';
+
+describe('ng-cloak', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should get removed when an element is compiled', inject(function($rootScope, $compile) {
+ element = jqLite('<div ng-cloak></div>');
+ expect(element.attr('ng-cloak')).toBe('');
+ $compile(element);
+ expect(element.attr('ng-cloak')).toBeUndefined();
+ }));
+
+
+ it('should remove ng-cloak class from a compiled element with attribute', inject(
+ function($rootScope, $compile) {
+ element = jqLite('<div ng-cloak class="foo ng-cloak bar"></div>');
+
+ expect(element.hasClass('foo')).toBe(true);
+ expect(element.hasClass('ng-cloak')).toBe(true);
+ expect(element.hasClass('bar')).toBe(true);
+
+ $compile(element);
+
+ expect(element.hasClass('foo')).toBe(true);
+ expect(element.hasClass('ng-cloak')).toBe(false);
+ expect(element.hasClass('bar')).toBe(true);
+ }));
+
+
+ it('should remove ng-cloak class from a compiled element', inject(function($rootScope, $compile) {
+ element = jqLite('<div class="foo ng-cloak bar"></div>');
+
+ expect(element.hasClass('foo')).toBe(true);
+ expect(element.hasClass('ng-cloak')).toBe(true);
+ expect(element.hasClass('bar')).toBe(true);
+
+ $compile(element);
+
+ expect(element.hasClass('foo')).toBe(true);
+ expect(element.hasClass('ng-cloak')).toBe(false);
+ expect(element.hasClass('bar')).toBe(true);
+ }));
+});
diff --git a/test/ng/directive/ngControllerSpec.js b/test/ng/directive/ngControllerSpec.js
new file mode 100644
index 00000000..832a683d
--- /dev/null
+++ b/test/ng/directive/ngControllerSpec.js
@@ -0,0 +1,65 @@
+'use strict';
+
+describe('ng-controller', function() {
+ var element;
+
+ beforeEach(inject(function($window) {
+ $window.Greeter = function($scope) {
+ // private stuff (not exported to scope)
+ this.prefix = 'Hello ';
+
+ // public stuff (exported to scope)
+ var ctrl = this;
+ $scope.name = 'Misko';
+ $scope.greet = function(name) {
+ return ctrl.prefix + name + ctrl.suffix;
+ };
+
+ $scope.protoGreet = bind(this, this.protoGreet);
+ };
+ $window.Greeter.prototype = {
+ suffix: '!',
+ protoGreet: function(name) {
+ return this.prefix + name + this.suffix;
+ }
+ };
+
+ $window.Child = function($scope) {
+ $scope.name = 'Adam';
+ };
+ }));
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should instantiate controller and bind methods', inject(function($compile, $rootScope) {
+ element = $compile('<div ng-controller="Greeter">{{greet(name)}}</div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello Misko!');
+ }));
+
+
+ it('should allow nested controllers', inject(function($compile, $rootScope) {
+ element = $compile('<div ng-controller="Greeter"><div ng-controller="Child">{{greet(name)}}</div></div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello Adam!');
+ dealoc(element);
+
+ element = $compile('<div ng-controller="Greeter"><div ng-controller="Child">{{protoGreet(name)}}</div></div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello Adam!');
+ }));
+
+
+ it('should instantiate controller defined on scope', inject(function($compile, $rootScope) {
+ $rootScope.Greeter = function($scope) {
+ $scope.name = 'Vojta';
+ };
+
+ element = $compile('<div ng-controller="Greeter">{{name}}</div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Vojta');
+ }));
+});
diff --git a/test/ng/directive/ngEventDirsSpec.js b/test/ng/directive/ngEventDirsSpec.js
new file mode 100644
index 00000000..c42f9b26
--- /dev/null
+++ b/test/ng/directive/ngEventDirsSpec.js
@@ -0,0 +1,25 @@
+'use strict';
+
+describe('event directives', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ describe('ng-submit', function() {
+
+ it('should get called on form submit', inject(function($rootScope, $compile) {
+ element = $compile('<form action="" ng-submit="submitted = true">' +
+ '<input type="submit"/>' +
+ '</form>')($rootScope);
+ $rootScope.$digest();
+ expect($rootScope.submitted).not.toBeDefined();
+
+ browserTrigger(element.children()[0]);
+ expect($rootScope.submitted).toEqual(true);
+ }));
+ });
+});
diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js
new file mode 100644
index 00000000..ab63dd02
--- /dev/null
+++ b/test/ng/directive/ngIncludeSpec.js
@@ -0,0 +1,289 @@
+'use strict';
+
+describe('ng-include', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ function putIntoCache(url, content) {
+ return function($templateCache) {
+ $templateCache.put(url, [200, content, {}]);
+ };
+ }
+
+
+ it('should include on external file', inject(putIntoCache('myUrl', '{{name}}'),
+ function($rootScope, $compile) {
+ element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
+ jqLite(document.body).append(element);
+ element = $compile(element)($rootScope);
+ $rootScope.childScope = $rootScope.$new();
+ $rootScope.childScope.name = 'misko';
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko');
+ jqLite(document.body).html('');
+ }));
+
+
+ it('should support ng-include="src" syntax', inject(putIntoCache('myUrl', '{{name}}'),
+ function($rootScope, $compile) {
+ element = jqLite('<div ng-include="url"></div>');
+ jqLite(document.body).append(element);
+ element = $compile(element)($rootScope);
+ $rootScope.name = 'Alibaba';
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Alibaba');
+ jqLite(document.body).html('');
+ }));
+
+
+ it('should remove previously included text if a falsy value is bound to src', inject(
+ putIntoCache('myUrl', '{{name}}'),
+ function($rootScope, $compile) {
+ element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
+ element = $compile(element)($rootScope);
+ $rootScope.childScope = $rootScope.$new();
+ $rootScope.childScope.name = 'igor';
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+
+ expect(element.text()).toEqual('igor');
+
+ $rootScope.url = undefined;
+ $rootScope.$digest();
+
+ expect(element.text()).toEqual('');
+ }));
+
+
+ it('should allow this for scope', inject(putIntoCache('myUrl', '{{"abc"}}'),
+ function($rootScope, $compile) {
+ element = jqLite('<ng:include src="url" scope="this"></ng:include>');
+ element = $compile(element)($rootScope);
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+
+ // TODO(misko): because we are using scope==this, the eval gets registered
+ // during the flush phase and hence does not get called.
+ // I don't think passing 'this' makes sense. Does having scope on ng-include makes sense?
+ // should we make scope="this" illegal?
+ $rootScope.$digest();
+
+ expect(element.text()).toEqual('abc');
+ }));
+
+
+ it('should fire $includeContentLoaded event after linking the content', inject(
+ function($rootScope, $compile, $templateCache) {
+ var contentLoadedSpy = jasmine.createSpy('content loaded').andCallFake(function() {
+ expect(element.text()).toBe('partial content');
+ });
+
+ $templateCache.put('url', [200, 'partial content', {}]);
+ $rootScope.$on('$includeContentLoaded', contentLoadedSpy);
+
+ element = $compile('<ng:include src="\'url\'"></ng:include>')($rootScope);
+ $rootScope.$digest();
+
+ expect(contentLoadedSpy).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should evaluate onload expression when a partial is loaded', inject(
+ putIntoCache('myUrl', 'my partial'),
+ function($rootScope, $compile) {
+ element = jqLite('<ng:include src="url" onload="loaded = true"></ng:include>');
+ element = $compile(element)($rootScope);
+
+ expect($rootScope.loaded).not.toBeDefined();
+
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+
+ expect(element.text()).toEqual('my partial');
+ expect($rootScope.loaded).toBe(true);
+ }));
+
+
+ it('should destroy old scope', inject(putIntoCache('myUrl', 'my partial'),
+ function($rootScope, $compile) {
+ element = jqLite('<ng:include src="url"></ng:include>');
+ element = $compile(element)($rootScope);
+
+ expect($rootScope.$$childHead).toBeFalsy();
+
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+ expect($rootScope.$$childHead).toBeTruthy();
+
+ $rootScope.url = null;
+ $rootScope.$digest();
+ expect($rootScope.$$childHead).toBeFalsy();
+ }));
+
+
+ it('should do xhr request and cache it',
+ inject(function($rootScope, $httpBackend, $compile) {
+ element = $compile('<ng:include src="url"></ng:include>')($rootScope);
+ $httpBackend.expect('GET', 'myUrl').respond('my partial');
+
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(element.text()).toEqual('my partial');
+
+ $rootScope.url = null;
+ $rootScope.$digest();
+ expect(element.text()).toEqual('');
+
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('my partial');
+ dealoc($rootScope);
+ }));
+
+
+ it('should clear content when error during xhr request',
+ inject(function($httpBackend, $compile, $rootScope) {
+ element = $compile('<ng:include src="url">content</ng:include>')($rootScope);
+ $httpBackend.expect('GET', 'myUrl').respond(404, '');
+
+ $rootScope.url = 'myUrl';
+ $rootScope.$digest();
+ $httpBackend.flush();
+
+ expect(element.text()).toBe('');
+ }));
+
+
+ it('should be async even if served from cache', inject(
+ putIntoCache('myUrl', 'my partial'),
+ function($rootScope, $compile) {
+ element = $compile('<ng:include src="url"></ng:include>')($rootScope);
+
+ $rootScope.url = 'myUrl';
+
+ var called = 0;
+ // we want to assert only during first watch
+ $rootScope.$watch(function() {
+ if (!called++) expect(element.text()).toBe('');
+ });
+
+ $rootScope.$digest();
+ expect(element.text()).toBe('my partial');
+ }));
+
+
+ it('should discard pending xhr callbacks if a new template is requested before the current ' +
+ 'finished loading', inject(function($rootScope, $compile, $httpBackend) {
+ element = jqLite("<ng:include src='templateUrl'></ng:include>");
+ var log = [];
+
+ $rootScope.templateUrl = 'myUrl1';
+ $rootScope.logger = function(msg) {
+ log.push(msg);
+ }
+ $compile(element)($rootScope);
+ expect(log.join('; ')).toEqual('');
+
+ $httpBackend.expect('GET', 'myUrl1').respond('<div>{{logger("url1")}}</div>');
+ $rootScope.$digest();
+ expect(log.join('; ')).toEqual('');
+ $rootScope.templateUrl = 'myUrl2';
+ $httpBackend.expect('GET', 'myUrl2').respond('<div>{{logger("url2")}}</div>');
+ $rootScope.$digest();
+ $httpBackend.flush(); // now that we have two requests pending, flush!
+
+ expect(log.join('; ')).toEqual('url2; url2'); // it's here twice because we go through at
+ // least two digest cycles
+ }));
+
+
+ it('should compile only the content', inject(function($compile, $rootScope, $templateCache) {
+ // regression
+
+ var onload = jasmine.createSpy('$includeContentLoaded');
+ $rootScope.$on('$includeContentLoaded', onload);
+ $templateCache.put('tpl.html', [200, 'partial {{tpl}}', {}]);
+
+ element = $compile('<div><div ng-repeat="i in [1]">' +
+ '<ng:include src="tpl"></ng:include></div></div>')($rootScope);
+ expect(onload).not.toHaveBeenCalled();
+
+ $rootScope.$apply(function() {
+ $rootScope.tpl = 'tpl.html';
+ });
+ expect(onload).toHaveBeenCalledOnce();
+ }));
+
+
+ describe('autoscoll', function() {
+ var autoScrollSpy;
+
+ function spyOnAnchorScroll() {
+ return function($provide) {
+ autoScrollSpy = jasmine.createSpy('$anchorScroll');
+ $provide.value('$anchorScroll', autoScrollSpy);
+ };
+ }
+
+ function compileAndLink(tpl) {
+ return function($compile, $rootScope) {
+ element = $compile(tpl)($rootScope);
+ };
+ }
+
+ function changeTplAndValueTo(template, value) {
+ return function($rootScope, $browser) {
+ $rootScope.$apply(function() {
+ $rootScope.tpl = template;
+ $rootScope.value = value;
+ });
+ };
+ }
+
+ beforeEach(module(spyOnAnchorScroll()));
+ beforeEach(inject(
+ putIntoCache('template.html', 'CONTENT'),
+ putIntoCache('another.html', 'CONTENT')));
+
+
+ it('should call $anchorScroll if autoscroll attribute is present', inject(
+ compileAndLink('<ng:include src="tpl" autoscroll></ng:include>'),
+ changeTplAndValueTo('template.html'), function() {
+ expect(autoScrollSpy).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should call $anchorScroll if autoscroll evaluates to true', inject(
+ compileAndLink('<ng:include src="tpl" autoscroll="value"></ng:include>'),
+ changeTplAndValueTo('template.html', true),
+ changeTplAndValueTo('another.html', 'some-string'),
+ changeTplAndValueTo('template.html', 100), function() {
+ expect(autoScrollSpy).toHaveBeenCalled();
+ expect(autoScrollSpy.callCount).toBe(3);
+ }));
+
+
+ it('should not call $anchorScroll if autoscroll attribute is not present', inject(
+ compileAndLink('<ng:include src="tpl"></ng:include>'),
+ changeTplAndValueTo('template.html'), function() {
+ expect(autoScrollSpy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should not call $anchorScroll if autoscroll evaluates to false', inject(
+ compileAndLink('<ng:include src="tpl" autoscroll="value"></ng:include>'),
+ changeTplAndValueTo('template.html', false),
+ changeTplAndValueTo('template.html', undefined),
+ changeTplAndValueTo('template.html', null), function() {
+ expect(autoScrollSpy).not.toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/test/ng/directive/ngInitSpec.js b/test/ng/directive/ngInitSpec.js
new file mode 100644
index 00000000..92146089
--- /dev/null
+++ b/test/ng/directive/ngInitSpec.js
@@ -0,0 +1,16 @@
+'use strict';
+
+describe('ng-init', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it("should ng-init", inject(function($rootScope, $compile) {
+ element = $compile('<div ng-init="a=123"></div>')($rootScope);
+ expect($rootScope.a).toEqual(123);
+ }));
+});
diff --git a/test/ng/directive/ngNonBindableSpec.js b/test/ng/directive/ngNonBindableSpec.js
new file mode 100644
index 00000000..1f7bf25d
--- /dev/null
+++ b/test/ng/directive/ngNonBindableSpec.js
@@ -0,0 +1,21 @@
+'use strict';
+
+
+describe('ng-non-bindable', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ it('should prevent compilation of the owning element and its children',
+ inject(function($rootScope, $compile) {
+ element = $compile('<div ng-non-bindable text="{{name}}"><span ng-bind="name"></span></div>')($rootScope);
+ $rootScope.name = 'misko';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('');
+ expect(element.attr('text')).toEqual('{{name}}');
+ }));
+});
diff --git a/test/ng/directive/ngPluralizeSpec.js b/test/ng/directive/ngPluralizeSpec.js
new file mode 100644
index 00000000..c7766c7b
--- /dev/null
+++ b/test/ng/directive/ngPluralizeSpec.js
@@ -0,0 +1,136 @@
+'use strict';
+
+describe('ng-pluralize', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ describe('deal with pluralized strings without offset', function() {
+ beforeEach(inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ng:pluralize count="email"' +
+ "when=\"{'0': 'You have no new email'," +
+ "'one': 'You have one new email'," +
+ "'other': 'You have {} new emails'}\">" +
+ '</ng:pluralize>')($rootScope);
+ }));
+
+
+ it('should show single/plural strings', inject(function($rootScope) {
+ $rootScope.email = 0;
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have no new email');
+
+ $rootScope.email = '0';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have no new email');
+
+ $rootScope.email = 1;
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have one new email');
+
+ $rootScope.email = 0.01;
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have 0.01 new emails');
+
+ $rootScope.email = '0.1';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have 0.1 new emails');
+
+ $rootScope.email = 2;
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have 2 new emails');
+
+ $rootScope.email = -0.1;
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have -0.1 new emails');
+
+ $rootScope.email = '-0.01';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have -0.01 new emails');
+
+ $rootScope.email = -2;
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have -2 new emails');
+ }));
+
+
+ it('should show single/plural strings with mal-formed inputs', inject(function($rootScope) {
+ $rootScope.email = '';
+ $rootScope.$digest();
+ expect(element.text()).toBe('');
+
+ $rootScope.email = null;
+ $rootScope.$digest();
+ expect(element.text()).toBe('');
+
+ $rootScope.email = undefined;
+ $rootScope.$digest();
+ expect(element.text()).toBe('');
+
+ $rootScope.email = 'a3';
+ $rootScope.$digest();
+ expect(element.text()).toBe('');
+
+ $rootScope.email = '011';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have 11 new emails');
+
+ $rootScope.email = '-011';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have -11 new emails');
+
+ $rootScope.email = '1fff';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have one new email');
+
+ $rootScope.email = '0aa22';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have no new email');
+
+ $rootScope.email = '000001';
+ $rootScope.$digest();
+ expect(element.text()).toBe('You have one new email');
+ }));
+ });
+
+
+ describe('deal with pluralized strings with offset', function() {
+ it('should show single/plural strings with offset', inject(function($rootScope, $compile) {
+ element = $compile(
+ "<ng:pluralize count=\"viewCount\" offset=2 " +
+ "when=\"{'0': 'Nobody is viewing.'," +
+ "'1': '{{p1}} is viewing.'," +
+ "'2': '{{p1}} and {{p2}} are viewing.'," +
+ "'one': '{{p1}}, {{p2}} and one other person are viewing.'," +
+ "'other': '{{p1}}, {{p2}} and {} other people are viewing.'}\">" +
+ "</ng:pluralize>")($rootScope);
+ $rootScope.p1 = 'Igor';
+ $rootScope.p2 = 'Misko';
+
+ $rootScope.viewCount = 0;
+ $rootScope.$digest();
+ expect(element.text()).toBe('Nobody is viewing.');
+
+ $rootScope.viewCount = 1;
+ $rootScope.$digest();
+ expect(element.text()).toBe('Igor is viewing.');
+
+ $rootScope.viewCount = 2;
+ $rootScope.$digest();
+ expect(element.text()).toBe('Igor and Misko are viewing.');
+
+ $rootScope.viewCount = 3;
+ $rootScope.$digest();
+ expect(element.text()).toBe('Igor, Misko and one other person are viewing.');
+
+ $rootScope.viewCount = 4;
+ $rootScope.$digest();
+ expect(element.text()).toBe('Igor, Misko and 2 other people are viewing.');
+ }));
+ });
+});
diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js
new file mode 100644
index 00000000..85aa1511
--- /dev/null
+++ b/test/ng/directive/ngRepeatSpec.js
@@ -0,0 +1,289 @@
+'use strict';
+
+describe('ng-repeat', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ it('should ng-repeat over array', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items" ng-init="suffix = \';\'" ng-bind="item + suffix"></li>' +
+ '</ul>')($rootScope);
+
+ Array.prototype.extraProperty = "should be ignored";
+ // INIT
+ $rootScope.items = ['misko', 'shyam'];
+ $rootScope.$digest();
+ expect(element.find('li').length).toEqual(2);
+ expect(element.text()).toEqual('misko;shyam;');
+ delete Array.prototype.extraProperty;
+
+ // GROW
+ $rootScope.items = ['adam', 'kai', 'brad'];
+ $rootScope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('adam;kai;brad;');
+
+ // SHRINK
+ $rootScope.items = ['brad'];
+ $rootScope.$digest();
+ expect(element.find('li').length).toEqual(1);
+ expect(element.text()).toEqual('brad;');
+ }));
+
+
+ it('should ng-repeat over object', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="(key, value) in items" ng-bind="key + \':\' + value + \';\' "></li>' +
+ '</ul>')($rootScope);
+ $rootScope.items = {misko:'swe', shyam:'set'};
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko:swe;shyam:set;');
+ }));
+
+
+ it('should not ng-repeat over parent properties', inject(function($rootScope, $compile) {
+ var Class = function() {};
+ Class.prototype.abc = function() {};
+ Class.prototype.value = 'abc';
+
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="(key, value) in items" ng-bind="key + \':\' + value + \';\' "></li>' +
+ '</ul>')($rootScope);
+ $rootScope.items = new Class();
+ $rootScope.items.name = 'value';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('name:value;');
+ }));
+
+
+ it('should error on wrong parsing of ng-repeat', inject(function($rootScope, $compile) {
+ expect(function() {
+ element = $compile('<ul><li ng-repeat="i dont parse"></li></ul>')($rootScope);
+ }).toThrow("Expected ng-repeat in form of '_item_ in _collection_' but got 'i dont parse'.");
+ }));
+
+
+ it("should throw error when left-hand-side of ng-repeat can't be parsed", inject(
+ function($rootScope, $compile) {
+ expect(function() {
+ element = $compile('<ul><li ng-repeat="i dont parse in foo"></li></ul>')($rootScope);
+ }).toThrow("'item' in 'item in collection' should be identifier or (key, value) but got " +
+ "'i dont parse'.");
+ }));
+
+
+ it('should expose iterator offset as $index when iterating over arrays',
+ inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items" ng-bind="item + $index + \'|\'"></li>' +
+ '</ul>')($rootScope);
+ $rootScope.items = ['misko', 'shyam', 'frodo'];
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko0|shyam1|frodo2|');
+ }));
+
+
+ it('should expose iterator offset as $index when iterating over objects',
+ inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="(key, val) in items" ng-bind="key + \':\' + val + $index + \'|\'"></li>' +
+ '</ul>')($rootScope);
+ $rootScope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'};
+ $rootScope.$digest();
+ expect(element.text()).toEqual('frodo:f0|misko:m1|shyam:s2|');
+ }));
+
+
+ it('should expose iterator position as $position when iterating over arrays',
+ inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items" ng-bind="item + \':\' + $position + \'|\'"></li>' +
+ '</ul>')($rootScope);
+ $rootScope.items = ['misko', 'shyam', 'doug'];
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko:first|shyam:middle|doug:last|');
+
+ $rootScope.items.push('frodo');
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko:first|shyam:middle|doug:middle|frodo:last|');
+
+ $rootScope.items.pop();
+ $rootScope.items.pop();
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko:first|shyam:last|');
+ }));
+
+
+ it('should expose iterator position as $position when iterating over objects',
+ inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="(key, val) in items" ng-bind="key + \':\' + val + \':\' + $position + \'|\'">' +
+ '</li>' +
+ '</ul>')($rootScope);
+ $rootScope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'};
+ $rootScope.$digest();
+ expect(element.text()).toEqual('doug:d:first|frodo:f:middle|misko:m:middle|shyam:s:last|');
+
+ delete $rootScope.items.doug;
+ delete $rootScope.items.frodo;
+ $rootScope.$digest();
+ expect(element.text()).toEqual('misko:m:first|shyam:s:last|');
+ }));
+
+
+ it('should ignore $ and $$ properties', inject(function($rootScope, $compile) {
+ element = $compile('<ul><li ng-repeat="i in items">{{i}}|</li></ul>')($rootScope);
+ $rootScope.items = ['a', 'b', 'c'];
+ $rootScope.items.$$hashkey = 'xxx';
+ $rootScope.items.$root = 'yyy';
+ $rootScope.$digest();
+
+ expect(element.text()).toEqual('a|b|c|');
+ }));
+
+
+ it('should repeat over nested arrays', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="subgroup in groups">' +
+ '<div ng-repeat="group in subgroup">{{group}}|</div>X' +
+ '</li>' +
+ '</ul>')($rootScope);
+ $rootScope.groups = [['a', 'b'], ['c','d']];
+ $rootScope.$digest();
+
+ expect(element.text()).toEqual('a|b|Xc|d|X');
+ }));
+
+
+ it('should ignore non-array element properties when iterating over an array',
+ inject(function($rootScope, $compile) {
+ element = $compile('<ul><li ng-repeat="item in array">{{item}}|</li></ul>')($rootScope);
+ $rootScope.array = ['a', 'b', 'c'];
+ $rootScope.array.foo = '23';
+ $rootScope.array.bar = function() {};
+ $rootScope.$digest();
+
+ expect(element.text()).toBe('a|b|c|');
+ }));
+
+
+ it('should iterate over non-existent elements of a sparse array',
+ inject(function($rootScope, $compile) {
+ element = $compile('<ul><li ng-repeat="item in array">{{item}}|</li></ul>')($rootScope);
+ $rootScope.array = ['a', 'b'];
+ $rootScope.array[4] = 'c';
+ $rootScope.array[6] = 'd';
+ $rootScope.$digest();
+
+ expect(element.text()).toBe('a|b|||c||d|');
+ }));
+
+
+ it('should iterate over all kinds of types', inject(function($rootScope, $compile) {
+ element = $compile('<ul><li ng-repeat="item in array">{{item}}|</li></ul>')($rootScope);
+ $rootScope.array = ['a', 1, null, undefined, {}];
+ $rootScope.$digest();
+
+ expect(element.text()).toMatch(/a\|1\|\|\|\{\s*\}\|/);
+ }));
+
+
+ describe('stability', function() {
+ var a, b, c, d, lis;
+
+ beforeEach(inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ul>' +
+ '<li ng-repeat="item in items" ng-bind="key + \':\' + val + \':\' + $position + \'|\'"></li>' +
+ '</ul>')($rootScope);
+ a = {};
+ b = {};
+ c = {};
+ d = {};
+
+ $rootScope.items = [a, b, c];
+ $rootScope.$digest();
+ lis = element.find('li');
+ }));
+
+
+ it('should preserve the order of elements', inject(function($rootScope) {
+ $rootScope.items = [a, c, d];
+ $rootScope.$digest();
+ var newElements = element.find('li');
+ expect(newElements[0]).toEqual(lis[0]);
+ expect(newElements[1]).toEqual(lis[2]);
+ expect(newElements[2]).not.toEqual(lis[1]);
+ }));
+
+
+ it('should support duplicates', inject(function($rootScope) {
+ $rootScope.items = [a, a, b, c];
+ $rootScope.$digest();
+ var newElements = element.find('li');
+ expect(newElements[0]).toEqual(lis[0]);
+ expect(newElements[1]).not.toEqual(lis[0]);
+ expect(newElements[2]).toEqual(lis[1]);
+ expect(newElements[3]).toEqual(lis[2]);
+
+ lis = newElements;
+ $rootScope.$digest();
+ newElements = element.find('li');
+ expect(newElements[0]).toEqual(lis[0]);
+ expect(newElements[1]).toEqual(lis[1]);
+ expect(newElements[2]).toEqual(lis[2]);
+ expect(newElements[3]).toEqual(lis[3]);
+
+ $rootScope.$digest();
+ newElements = element.find('li');
+ expect(newElements[0]).toEqual(lis[0]);
+ expect(newElements[1]).toEqual(lis[1]);
+ expect(newElements[2]).toEqual(lis[2]);
+ expect(newElements[3]).toEqual(lis[3]);
+ }));
+
+
+ it('should remove last item when one duplicate instance is removed',
+ inject(function($rootScope) {
+ $rootScope.items = [a, a, a];
+ $rootScope.$digest();
+ lis = element.find('li');
+
+ $rootScope.items = [a, a];
+ $rootScope.$digest();
+ var newElements = element.find('li');
+ expect(newElements.length).toEqual(2);
+ expect(newElements[0]).toEqual(lis[0]);
+ expect(newElements[1]).toEqual(lis[1]);
+ }));
+
+
+ it('should reverse items when the collection is reversed',
+ inject(function($rootScope) {
+ $rootScope.items = [a, b, c];
+ $rootScope.$digest();
+ lis = element.find('li');
+
+ $rootScope.items = [c, b, a];
+ $rootScope.$digest();
+ var newElements = element.find('li');
+ expect(newElements.length).toEqual(3);
+ expect(newElements[0]).toEqual(lis[2]);
+ expect(newElements[1]).toEqual(lis[1]);
+ expect(newElements[2]).toEqual(lis[0]);
+ }));
+ });
+});
diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js
new file mode 100644
index 00000000..5005274d
--- /dev/null
+++ b/test/ng/directive/ngShowHideSpec.js
@@ -0,0 +1,43 @@
+'use strict';
+
+describe('ng-show / ng-hide', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+ describe('ng-show', function() {
+ it('should show and hide an element', inject(function($rootScope, $compile) {
+ element = jqLite('<div ng-show="exp"></div>');
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect(isCssVisible(element)).toEqual(false);
+ $rootScope.exp = true;
+ $rootScope.$digest();
+ expect(isCssVisible(element)).toEqual(true);
+ }));
+
+
+ it('should make hidden element visible', inject(function($rootScope, $compile) {
+ element = jqLite('<div style="display: none" ng-show="exp"></div>');
+ element = $compile(element)($rootScope);
+ expect(isCssVisible(element)).toBe(false);
+ $rootScope.exp = true;
+ $rootScope.$digest();
+ expect(isCssVisible(element)).toBe(true);
+ }));
+ });
+
+ describe('ng-hide', function() {
+ it('should hide an element', inject(function($rootScope, $compile) {
+ element = jqLite('<div ng-hide="exp"></div>');
+ element = $compile(element)($rootScope);
+ expect(isCssVisible(element)).toBe(true);
+ $rootScope.exp = true;
+ $rootScope.$digest();
+ expect(isCssVisible(element)).toBe(false);
+ }));
+ });
+});
diff --git a/test/ng/directive/ngStyleSpec.js b/test/ng/directive/ngStyleSpec.js
new file mode 100644
index 00000000..c12f2f4d
--- /dev/null
+++ b/test/ng/directive/ngStyleSpec.js
@@ -0,0 +1,88 @@
+'use strict';
+
+describe('ng-style', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should set', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-style="{height: \'40px\'}"></div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.css('height')).toEqual('40px');
+ }));
+
+
+ it('should silently ignore undefined style', inject(function($rootScope, $compile) {
+ element = $compile('<div ng-style="myStyle"></div>')($rootScope);
+ $rootScope.$digest();
+ expect(element.hasClass('ng-exception')).toBeFalsy();
+ }));
+
+
+ describe('preserving styles set before and after compilation', function() {
+ var scope, preCompStyle, preCompVal, postCompStyle, postCompVal, element;
+
+ beforeEach(inject(function($rootScope, $compile) {
+ preCompStyle = 'width';
+ preCompVal = '300px';
+ postCompStyle = 'height';
+ postCompVal = '100px';
+ element = jqLite('<div ng-style="styleObj"></div>');
+ element.css(preCompStyle, preCompVal);
+ jqLite(document.body).append(element);
+ $compile(element)($rootScope);
+ scope = $rootScope;
+ scope.styleObj = {'margin-top': '44px'};
+ scope.$apply();
+ element.css(postCompStyle, postCompVal);
+ }));
+
+ afterEach(function() {
+ element.remove();
+ });
+
+
+ it('should not mess up stuff after compilation', function() {
+ element.css('margin', '44px');
+ expect(element.css(preCompStyle)).toBe(preCompVal);
+ expect(element.css('margin-top')).toBe('44px');
+ expect(element.css(postCompStyle)).toBe(postCompVal);
+ });
+
+
+ it('should not mess up stuff after $apply with no model changes', function() {
+ element.css('padding-top', '33px');
+ scope.$apply();
+ expect(element.css(preCompStyle)).toBe(preCompVal);
+ expect(element.css('margin-top')).toBe('44px');
+ expect(element.css(postCompStyle)).toBe(postCompVal);
+ expect(element.css('padding-top')).toBe('33px');
+ });
+
+
+ it('should not mess up stuff after $apply with non-colliding model changes', function() {
+ scope.styleObj = {'padding-top': '99px'};
+ scope.$apply();
+ expect(element.css(preCompStyle)).toBe(preCompVal);
+ expect(element.css('margin-top')).not.toBe('44px');
+ expect(element.css('padding-top')).toBe('99px');
+ expect(element.css(postCompStyle)).toBe(postCompVal);
+ });
+
+
+ it('should overwrite original styles after a colliding model change', function() {
+ scope.styleObj = {'height': '99px', 'width': '88px'};
+ scope.$apply();
+ expect(element.css(preCompStyle)).toBe('88px');
+ expect(element.css(postCompStyle)).toBe('99px');
+ scope.styleObj = {};
+ scope.$apply();
+ expect(element.css(preCompStyle)).not.toBe('88px');
+ expect(element.css(postCompStyle)).not.toBe('99px');
+ });
+ });
+});
diff --git a/test/ng/directive/ngSwitchSpec.js b/test/ng/directive/ngSwitchSpec.js
new file mode 100644
index 00000000..b4df109e
--- /dev/null
+++ b/test/ng/directive/ngSwitchSpec.js
@@ -0,0 +1,93 @@
+'use strict';
+
+describe('ng-switch', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ it('should switch on value change', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<div ng-switch="select">' +
+ '<div ng-switch-when="1">first:{{name}}</div>' +
+ '<div ng-switch-when="2">second:{{name}}</div>' +
+ '<div ng-switch-when="true">true:{{name}}</div>' +
+ '</div>')($rootScope);
+ expect(element.html()).toEqual(
+ '<!-- ngSwitchWhen: 1 --><!-- ngSwitchWhen: 2 --><!-- ngSwitchWhen: true -->');
+ $rootScope.select = 1;
+ $rootScope.$apply();
+ expect(element.text()).toEqual('first:');
+ $rootScope.name="shyam";
+ $rootScope.$apply();
+ expect(element.text()).toEqual('first:shyam');
+ $rootScope.select = 2;
+ $rootScope.$apply();
+ expect(element.text()).toEqual('second:shyam');
+ $rootScope.name = 'misko';
+ $rootScope.$apply();
+ expect(element.text()).toEqual('second:misko');
+ $rootScope.select = true;
+ $rootScope.$apply();
+ expect(element.text()).toEqual('true:misko');
+ }));
+
+
+ it('should switch on switch-when-default', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ng:switch on="select">' +
+ '<div ng:switch-when="1">one</div>' +
+ '<div ng:switch-default>other</div>' +
+ '</ng:switch>')($rootScope);
+ $rootScope.$apply();
+ expect(element.text()).toEqual('other');
+ $rootScope.select = 1;
+ $rootScope.$apply();
+ expect(element.text()).toEqual('one');
+ }));
+
+
+ it('should call change on switch', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ng:switch on="url" change="name=\'works\'">' +
+ '<div ng-switch-when="a">{{name}}</div>' +
+ '</ng:switch>')($rootScope);
+ $rootScope.url = 'a';
+ $rootScope.$apply();
+ expect($rootScope.name).toEqual('works');
+ expect(element.text()).toEqual('works');
+ }));
+
+
+ it('should properly create and destroy child scopes', inject(function($rootScope, $compile) {
+ element = $compile(
+ '<ng:switch on="url">' +
+ '<div ng-switch-when="a">{{name}}</div>' +
+ '</ng:switch>')($rootScope);
+ $rootScope.$apply();
+
+ var getChildScope = function() { return element.find('div').scope(); };
+
+ expect(getChildScope()).toBeUndefined();
+
+ $rootScope.url = 'a';
+ $rootScope.$apply();
+ var child1 = getChildScope();
+ expect(child1).toBeDefined();
+ spyOn(child1, '$destroy');
+
+ $rootScope.url = 'x';
+ $rootScope.$apply();
+ expect(getChildScope()).toBeUndefined();
+ expect(child1.$destroy).toHaveBeenCalledOnce();
+
+ $rootScope.url = 'a';
+ $rootScope.$apply();
+ var child2 = getChildScope();
+ expect(child2).toBeDefined();
+ expect(child2).not.toBe(child1);
+ }));
+});
diff --git a/test/ng/directive/ngViewSpec.js b/test/ng/directive/ngViewSpec.js
new file mode 100644
index 00000000..636e15a8
--- /dev/null
+++ b/test/ng/directive/ngViewSpec.js
@@ -0,0 +1,459 @@
+'use strict';
+
+describe('ng-view', function() {
+ var element;
+
+ beforeEach(module(function() {
+ return function($rootScope, $compile) {
+ element = $compile('<ng:view onload="load()"></ng:view>')($rootScope);
+ };
+ }));
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ it('should do nothing when no routes are defined',
+ inject(function($rootScope, $compile, $location) {
+ $location.path('/unknown');
+ $rootScope.$digest();
+ expect(element.text()).toEqual('');
+ }));
+
+
+ it('should instantiate controller after compiling the content', function() {
+ var log = [], controllerScope,
+ Ctrl = function($scope) {
+ controllerScope = $scope;
+ log.push('ctrl-init');
+ };
+
+ module(function($compileProvider, $routeProvider) {
+ $compileProvider.directive('compileLog', function() {
+ return {
+ compile: function() {
+ log.push('compile');
+ }
+ };
+ });
+
+ $routeProvider.when('/some', {template: '/tpl.html', controller: Ctrl});
+ });
+
+ inject(function($route, $rootScope, $templateCache, $location) {
+ $templateCache.put('/tpl.html', [200, '<div compile-log>partial</div>', {}]);
+ $location.path('/some');
+ $rootScope.$digest();
+
+ expect(controllerScope.$parent).toBe($rootScope);
+ expect(controllerScope).toBe($route.current.scope);
+ expect(log).toEqual(['compile', 'ctrl-init']);
+ });
+ });
+
+
+ it('should support string controller declaration', function() {
+ var MyCtrl = jasmine.createSpy('MyCtrl');
+
+ module(function($controllerProvider, $routeProvider) {
+ $controllerProvider.register('MyCtrl', ['$scope', MyCtrl]);
+ $routeProvider.when('/foo', {controller: 'MyCtrl', template: '/tpl.html'});
+ });
+
+ inject(function($route, $location, $rootScope, $templateCache) {
+ $templateCache.put('/tpl.html', [200, '<div></div>', {}]);
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($route.current.controller).toBe('MyCtrl');
+ expect(MyCtrl).toHaveBeenCalledWith(element.contents().scope());
+ });
+ });
+
+
+ it('should load content via xhr when route changes', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'myUrl1'});
+ $routeProvider.when('/bar', {template: 'myUrl2'});
+ });
+
+ inject(function($rootScope, $compile, $httpBackend, $location, $route) {
+ expect(element.text()).toEqual('');
+
+ $location.path('/foo');
+ $httpBackend.expect('GET', 'myUrl1').respond('<div>{{1+3}}</div>');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(element.text()).toEqual('4');
+
+ $location.path('/bar');
+ $httpBackend.expect('GET', 'myUrl2').respond('angular is da best');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(element.text()).toEqual('angular is da best');
+ });
+ });
+
+
+ it('should remove all content when location changes to an unknown route', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'myUrl1'});
+ });
+
+ inject(function($rootScope, $compile, $location, $httpBackend, $route) {
+ $location.path('/foo');
+ $httpBackend.expect('GET', 'myUrl1').respond('<div>{{1+3}}</div>');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(element.text()).toEqual('4');
+
+ $location.path('/unknown');
+ $rootScope.$digest();
+ expect(element.text()).toEqual('');
+ });
+ });
+
+
+ it('should chain scopes and propagate evals to the child scope', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'myUrl1'});
+ });
+
+ inject(function($rootScope, $compile, $location, $httpBackend, $route) {
+ $rootScope.parentVar = 'parent';
+
+ $location.path('/foo');
+ $httpBackend.expect('GET', 'myUrl1').respond('<div>{{parentVar}}</div>');
+ $rootScope.$digest();
+ $httpBackend.flush();
+ expect(element.text()).toEqual('parent');
+
+ $rootScope.parentVar = 'new parent';
+ $rootScope.$digest();
+ expect(element.text()).toEqual('new parent');
+ });
+ });
+
+
+ it('should be possible to nest ng-view in ng-include', inject(function() {
+ // TODO(vojta): refactor this test
+ dealoc(element);
+ var injector = angular.injector(['ng', 'ngMock', function($routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop, template: 'viewPartial.html'});
+ }]);
+ var myApp = injector.get('$rootScope');
+ var $httpBackend = injector.get('$httpBackend');
+ $httpBackend.expect('GET', 'includePartial.html').respond('view: <ng:view></ng:view>');
+ injector.get('$location').path('/foo');
+
+ var $route = injector.get('$route');
+
+ element = injector.get('$compile')(
+ '<div>' +
+ 'include: <ng:include src="\'includePartial.html\'"> </ng:include>' +
+ '</div>')(myApp);
+ myApp.$apply();
+
+ $httpBackend.expect('GET', 'viewPartial.html').respond('content');
+ $httpBackend.flush();
+
+ expect(element.text()).toEqual('include: view: content');
+ expect($route.current.template).toEqual('viewPartial.html');
+ dealoc(myApp);
+ dealoc(element);
+ }));
+
+
+ it('should initialize view template after the view controller was initialized even when ' +
+ 'templates were cached', function() {
+ //this is a test for a regression that was introduced by making the ng-view cache sync
+ function ParentCtrl($scope) {
+ $scope.log.push('parent');
+ }
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: ParentCtrl, template: 'viewPartial.html'});
+ });
+
+
+ inject(function($rootScope, $compile, $location, $httpBackend, $route) {
+ $rootScope.log = [];
+
+ $rootScope.ChildCtrl = function($scope) {
+ $scope.log.push('child');
+ };
+
+ $location.path('/foo');
+ $httpBackend.expect('GET', 'viewPartial.html').
+ respond('<div ng-init="log.push(\'init\')">' +
+ '<div ng-controller="ChildCtrl"></div>' +
+ '</div>');
+ $rootScope.$apply();
+ $httpBackend.flush();
+
+ expect($rootScope.log).toEqual(['parent', 'init', 'child']);
+
+ $location.path('/');
+ $rootScope.$apply();
+ expect($rootScope.log).toEqual(['parent', 'init', 'child']);
+
+ $rootScope.log = [];
+ $location.path('/foo');
+ $rootScope.$apply();
+
+ expect($rootScope.log).toEqual(['parent', 'init', 'child']);
+ });
+ });
+
+
+ it('should discard pending xhr callbacks if a new route is requested before the current ' +
+ 'finished loading', function() {
+ // this is a test for a bad race condition that affected feedback
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'myUrl1'});
+ $routeProvider.when('/bar', {template: 'myUrl2'});
+ });
+
+ inject(function($route, $rootScope, $location, $httpBackend) {
+ expect(element.text()).toEqual('');
+
+ $location.path('/foo');
+ $httpBackend.expect('GET', 'myUrl1').respond('<div>{{1+3}}</div>');
+ $rootScope.$digest();
+ $location.path('/bar');
+ $httpBackend.expect('GET', 'myUrl2').respond('<div>{{1+1}}</div>');
+ $rootScope.$digest();
+ $httpBackend.flush(); // now that we have two requests pending, flush!
+
+ expect(element.text()).toEqual('2');
+ });
+ });
+
+
+ it('should clear the content when error during xhr request', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: noop, template: 'myUrl1'});
+ });
+
+ inject(function($route, $location, $rootScope, $httpBackend) {
+ $location.path('/foo');
+ $httpBackend.expect('GET', 'myUrl1').respond(404, '');
+ element.text('content');
+
+ $rootScope.$digest();
+ $httpBackend.flush();
+
+ expect(element.text()).toBe('');
+ });
+ });
+
+
+ it('should be async even if served from cache', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: noop, template: 'myUrl1'});
+ });
+
+ inject(function($route, $rootScope, $location, $templateCache) {
+ $templateCache.put('myUrl1', [200, 'my partial', {}]);
+ $location.path('/foo');
+
+ var called = 0;
+ // we want to assert only during first watch
+ $rootScope.$watch(function() {
+ if (!called++) expect(element.text()).toBe('');
+ });
+
+ $rootScope.$digest();
+ expect(element.text()).toBe('my partial');
+ });
+ });
+
+ it('should fire $contentLoaded event when content compiled and linked', function() {
+ var log = [];
+ var logger = function(name) {
+ return function() {
+ log.push(name);
+ };
+ };
+ var Ctrl = function($scope) {
+ $scope.value = 'bound-value';
+ log.push('init-ctrl');
+ };
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'tpl.html', controller: Ctrl});
+ });
+
+ inject(function($templateCache, $rootScope, $location) {
+ $rootScope.$on('$beforeRouteChange', logger('$beforeRouteChange'));
+ $rootScope.$on('$afterRouteChange', logger('$afterRouteChange'));
+ $rootScope.$on('$viewContentLoaded', logger('$viewContentLoaded'));
+
+ $templateCache.put('tpl.html', [200, '{{value}}', {}]);
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect(element.text()).toBe('bound-value');
+ expect(log).toEqual(['$beforeRouteChange', '$afterRouteChange', 'init-ctrl',
+ '$viewContentLoaded']);
+ });
+ });
+
+ it('should destroy previous scope', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'tpl.html'});
+ });
+
+ inject(function($templateCache, $rootScope, $location) {
+ $templateCache.put('tpl.html', [200, 'partial', {}]);
+
+ expect($rootScope.$$childHead).toBeNull();
+ expect($rootScope.$$childTail).toBeNull();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect(element.text()).toBe('partial');
+ expect($rootScope.$$childHead).not.toBeNull();
+ expect($rootScope.$$childTail).not.toBeNull();
+
+ $location.path('/non/existing/route');
+ $rootScope.$digest();
+
+ expect(element.text()).toBe('');
+ expect($rootScope.$$childHead).toBeNull();
+ expect($rootScope.$$childTail).toBeNull();
+ });
+ });
+
+
+ it('should destroy previous scope if multiple route changes occur before server responds',
+ function() {
+ var log = [];
+ var createCtrl = function(name) {
+ return function($scope) {
+ log.push('init-' + name);
+ $scope.$on('$destroy', function() {log.push('destroy-' + name);});
+ };
+ };
+
+ module(function($routeProvider) {
+ $routeProvider.when('/one', {template: 'one.html', controller: createCtrl('ctrl1')});
+ $routeProvider.when('/two', {template: 'two.html', controller: createCtrl('ctrl2')});
+ });
+
+ inject(function($httpBackend, $rootScope, $location) {
+ $httpBackend.whenGET('one.html').respond('content 1');
+ $httpBackend.whenGET('two.html').respond('content 2');
+
+ $location.path('/one');
+ $rootScope.$digest();
+ $location.path('/two');
+ $rootScope.$digest();
+
+ $httpBackend.flush();
+ expect(element.text()).toBe('content 2');
+ expect(log).toEqual(['init-ctrl2']);
+
+ $location.path('/non-existing');
+ $rootScope.$digest();
+
+ expect(element.text()).toBe('');
+ expect(log).toEqual(['init-ctrl2', 'destroy-ctrl2']);
+
+ expect($rootScope.$$childHead).toBeNull();
+ expect($rootScope.$$childTail).toBeNull();
+ });
+ });
+
+
+ it('should $destroy scope after update and reload', function() {
+ // this is a regression of bug, where $route doesn't copy scope when only updating
+
+ var log = [];
+
+ function logger(msg) {
+ return function() {
+ log.push(msg);
+ };
+ }
+
+ function createController(name) {
+ return function($scope) {
+ log.push('init-' + name);
+ $scope.$on('$destroy', logger('destroy-' + name));
+ $scope.$on('$routeUpdate', logger('route-update'));
+ };
+ }
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar', {template: 'tpl.html', controller: createController('bar')});
+ $routeProvider.when('/foo', {
+ template: 'tpl.html', controller: createController('foo'), reloadOnSearch: false});
+ });
+
+ inject(function($templateCache, $location, $rootScope) {
+ $templateCache.put('tpl.html', [200, 'partial', {}]);
+
+ $location.url('/foo');
+ $rootScope.$digest();
+ expect(log).toEqual(['init-foo']);
+
+ $location.search({q: 'some'});
+ $rootScope.$digest();
+ expect(log).toEqual(['init-foo', 'route-update']);
+
+ $location.url('/bar');
+ $rootScope.$digest();
+ expect(log).toEqual(['init-foo', 'route-update', 'destroy-foo', 'init-bar']);
+ });
+ });
+
+
+ it('should evaluate onload expression after linking the content', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'tpl.html'});
+ });
+
+ inject(function($templateCache, $location, $rootScope) {
+ $templateCache.put('tpl.html', [200, '{{1+1}}', {}]);
+ $rootScope.load = jasmine.createSpy('onload');
+
+ $location.url('/foo');
+ $rootScope.$digest();
+ expect($rootScope.load).toHaveBeenCalledOnce();
+ });
+ })
+
+
+ it('should set $scope and $controllerController on the view', function() {
+ function MyCtrl($scope) {
+ $scope.state = 'WORKS';
+ $scope.ctrl = this;
+ }
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'tpl.html', controller: MyCtrl});
+ });
+
+ inject(function($templateCache, $location, $rootScope, $route) {
+ $templateCache.put('tpl.html', [200, '<div>{{state}}</div>', {}]);
+
+ $location.url('/foo');
+ $rootScope.$digest();
+ expect(element.text()).toEqual('WORKS');
+
+ var div = element.find('div');
+ expect(nodeName_(div.parent())).toEqual('NG:VIEW');
+
+ expect(div.scope()).toBe($route.current.scope);
+ expect(div.scope().hasOwnProperty('state')).toBe(true);
+ expect(div.scope().state).toEqual('WORKS');
+
+ expect(div.controller()).toBe($route.current.scope.ctrl);
+ });
+ });
+});
diff --git a/test/ng/directive/scriptSpec.js b/test/ng/directive/scriptSpec.js
new file mode 100644
index 00000000..471e04ce
--- /dev/null
+++ b/test/ng/directive/scriptSpec.js
@@ -0,0 +1,44 @@
+'use strict';
+
+describe('scriptDirective', function() {
+ var element;
+
+
+ afterEach(function(){
+ dealoc(element);
+ });
+
+
+ it('should populate $templateCache with contents of a ng-template script element', inject(
+ function($compile, $templateCache) {
+ if (msie <=8) return;
+ // in ie8 it is not possible to create a script tag with the right content.
+ // it always comes up as empty. I was trying to set the text of the
+ // script tag, but that did not work either, so I gave up.
+ $compile('<div>foo' +
+ '<script id="/ignore">ignore me</script>' +
+ '<script type="text/ng-template" id="/myTemplate.html"><x>{{y}}</x></script>' +
+ '</div>' );
+ expect($templateCache.get('/myTemplate.html')).toBe('<x>{{y}}</x>');
+ expect($templateCache.get('/ignore')).toBeUndefined();
+ }
+ ));
+
+
+ it('should not compile scripts', inject(function($compile, $templateCache, $rootScope) {
+ if (msie <=8) return; // see above
+
+ var doc = jqLite('<div></div>');
+ // jQuery is too smart and removes
+ doc[0].innerHTML = '<script type="text/javascript">some {{binding}}</script>' +
+ '<script type="text/ng-template" id="/some">other {{binding}}</script>';
+
+ $compile(doc)($rootScope);
+ $rootScope.$digest();
+
+ var scripts = doc.find('script');
+ expect(scripts.eq(0).text()).toBe('some {{binding}}');
+ expect(scripts.eq(1).text()).toBe('other {{binding}}');
+ dealoc(doc);
+ }));
+});
diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js
new file mode 100644
index 00000000..2e3cfaaf
--- /dev/null
+++ b/test/ng/directive/selectSpec.js
@@ -0,0 +1,863 @@
+'use strict';
+
+describe('select', function() {
+ var scope, formElement, element, $compile;
+
+ function compile(html) {
+ formElement = jqLite('<form name="form">' + html + '</form>');
+ element = formElement.find('select');
+ $compile(formElement)(scope);
+ scope.$apply();
+ }
+
+ beforeEach(inject(function($injector, $rootScope) {
+ scope = $rootScope;
+ $compile = $injector.get('$compile');
+ formElement = element = null;
+ }));
+
+ afterEach(function() {
+ dealoc(formElement);
+ });
+
+
+ describe('select-one', function() {
+
+ it('should compile children of a select without a ng-model, but not create a model for it',
+ function() {
+ compile('<select>' +
+ '<option selected="true">{{a}}</option>' +
+ '<option value="">{{b}}</option>' +
+ '<option>C</option>' +
+ '</select>');
+ scope.$apply(function() {
+ scope.a = 'foo';
+ scope.b = 'bar';
+ });
+
+ expect(element.text()).toBe('foobarC');
+ });
+
+
+ it('should require', function() {
+ compile(
+ '<select name="select" ng-model="selection" required ng-change="change()">' +
+ '<option value=""></option>' +
+ '<option value="c">C</option>' +
+ '</select>');
+
+ scope.change = function() {
+ scope.log += 'change;';
+ };
+
+ scope.$apply(function() {
+ scope.log = '';
+ scope.selection = 'c';
+ });
+
+ expect(scope.form.select.$error.required).toBeFalsy();
+ expect(element).toBeValid();
+ expect(element).toBePristine();
+
+ scope.$apply(function() {
+ scope.selection = '';
+ });
+
+ expect(scope.form.select.$error.required).toBeTruthy();
+ expect(element).toBeInvalid();
+ expect(element).toBePristine();
+ expect(scope.log).toEqual('');
+
+ element[0].value = 'c';
+ browserTrigger(element, 'change');
+ expect(element).toBeValid();
+ expect(element).toBeDirty();
+ expect(scope.log).toEqual('change;');
+ });
+
+
+ it('should not be invalid if no require', function() {
+ compile(
+ '<select name="select" ng-model="selection">' +
+ '<option value=""></option>' +
+ '<option value="c">C</option>' +
+ '</select>');
+
+ expect(element).toBeValid();
+ expect(element).toBePristine();
+ });
+ });
+
+
+ describe('select-multiple', function() {
+
+ it('should support type="select-multiple"', function() {
+ compile(
+ '<select ng-model="selection" multiple>' +
+ '<option>A</option>' +
+ '<option>B</option>' +
+ '</select>');
+
+ scope.$apply(function() {
+ scope.selection = ['A'];
+ });
+
+ expect(element.find('option')[0].selected).toEqual(true);
+ expect(element.find('option')[1].selected).toEqual(false);
+
+ scope.$apply(function() {
+ scope.selection.push('B');
+ });
+
+ expect(element.find('option')[0].selected).toEqual(true);
+ expect(element.find('option')[1].selected).toEqual(true);
+ });
+
+
+ it('should require', function() {
+ compile(
+ '<select name="select" ng-model="selection" multiple required>' +
+ '<option>A</option>' +
+ '<option>B</option>' +
+ '</select>');
+
+ scope.$apply(function() {
+ scope.selection = [];
+ });
+
+ expect(scope.form.select.$error.required).toBeTruthy();
+ expect(element).toBeInvalid();
+ expect(element).toBePristine();
+
+ scope.$apply(function() {
+ scope.selection = ['A'];
+ });
+
+ expect(element).toBeValid();
+ expect(element).toBePristine();
+
+ element[0].value = 'B';
+ browserTrigger(element, 'change');
+ expect(element).toBeValid();
+ expect(element).toBeDirty();
+ });
+ });
+
+
+ describe('ng-options', function() {
+ function createSelect(attrs, blank, unknown) {
+ var html = '<select';
+ forEach(attrs, function(value, key) {
+ if (isBoolean(value)) {
+ if (value) html += ' ' + key;
+ } else {
+ html += ' ' + key + '="' + value + '"';
+ }
+ });
+ html += '>' +
+ (blank ? (isString(blank) ? blank : '<option value="">blank</option>') : '') +
+ (unknown ? (isString(unknown) ? unknown : '<option value="?">unknown</option>') : '') +
+ '</select>';
+
+ compile(html);
+ }
+
+ function createSingleSelect(blank, unknown) {
+ createSelect({
+ 'ng-model':'selected',
+ 'ng-options':'value.name for value in values'
+ }, blank, unknown);
+ }
+
+ function createMultiSelect(blank, unknown) {
+ createSelect({
+ 'ng-model':'selected',
+ 'multiple':true,
+ 'ng-options':'value.name for value in values'
+ }, blank, unknown);
+ }
+
+
+ it('should throw when not formated "? for ? in ?"', function() {
+ expect(function() {
+ compile('<select ng-model="selected" ng-options="i dont parse"></select>');
+ }).toThrow("Expected ng-options in form of '_select_ (as _label_)? for (_key_,)?_value_ in" +
+ " _collection_' but got 'i dont parse'.");
+ });
+
+
+ it('should render a list', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ var options = element.find('option');
+ expect(options.length).toEqual(3);
+ expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(options[1])).toEqual('<option value="1">B</option>');
+ expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>');
+ });
+
+
+ it('should render an object', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'value as key for (key, value) in object'
+ });
+
+ scope.$apply(function() {
+ scope.object = {'red': 'FF0000', 'green': '00FF00', 'blue': '0000FF'};
+ scope.selected = scope.object.red;
+ });
+
+ var options = element.find('option');
+ expect(options.length).toEqual(3);
+ expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>');
+ expect(sortedHtml(options[1])).toEqual('<option value="green">green</option>');
+ expect(sortedHtml(options[2])).toEqual('<option value="red">red</option>');
+ expect(options[2].selected).toEqual(true);
+
+ scope.$apply(function() {
+ scope.object.azur = '8888FF';
+ });
+
+ options = element.find('option');
+ expect(options[3].selected).toEqual(true);
+ });
+
+
+ it('should grow list', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [];
+ });
+
+ expect(element.find('option').length).toEqual(1); // because we add special empty option
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="?"></option>');
+
+ scope.$apply(function() {
+ scope.values.push({name:'A'});
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(1);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+
+ scope.$apply(function() {
+ scope.values.push({name:'B'});
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(element.find('option')[1])).toEqual('<option value="1">B</option>');
+ });
+
+
+ it('should shrink list', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(3);
+
+ scope.$apply(function() {
+ scope.values.pop();
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(element.find('option')[1])).toEqual('<option value="1">B</option>');
+
+ scope.$apply(function() {
+ scope.values.pop();
+ });
+
+ expect(element.find('option').length).toEqual(1);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+
+ scope.$apply(function() {
+ scope.values.pop();
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(1); // we add back the special empty option
+ });
+
+
+ it('should shrink and then grow list', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(3);
+
+ scope.$apply(function() {
+ scope.values = [{name: '1'}, {name: '2'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(2);
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(3);
+ });
+
+
+ it('should update list', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ scope.$apply(function() {
+ scope.values = [{name: 'B'}, {name: 'C'}, {name: 'D'}];
+ scope.selected = scope.values[0];
+ });
+
+ var options = element.find('option');
+ expect(options.length).toEqual(3);
+ expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>');
+ expect(sortedHtml(options[1])).toEqual('<option value="1">C</option>');
+ expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>');
+ });
+
+
+ it('should preserve existing options', function() {
+ createSingleSelect(true);
+
+ scope.$apply(function() {
+ scope.values = [];
+ });
+
+ expect(element.find('option').length).toEqual(1);
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
+ expect(jqLite(element.find('option')[1]).text()).toEqual('A');
+
+ scope.$apply(function() {
+ scope.values = [];
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(1);
+ expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
+ });
+
+
+ describe('binding', function() {
+
+ it('should bind to scope value', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[1];
+ });
+
+ expect(element.val()).toEqual('1');
+ });
+
+
+ it('should bind to scope value and group', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item.name group by item.group for item in values'
+ });
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'},
+ {name: 'B', group: 'first'},
+ {name: 'C', group: 'second'},
+ {name: 'D', group: 'first'},
+ {name: 'E', group: 'second'}];
+ scope.selected = scope.values[3];
+ });
+
+ expect(element.val()).toEqual('3');
+
+ var first = jqLite(element.find('optgroup')[0]);
+ var b = jqLite(first.find('option')[0]);
+ var d = jqLite(first.find('option')[1]);
+ expect(first.attr('label')).toEqual('first');
+ expect(b.text()).toEqual('B');
+ expect(d.text()).toEqual('D');
+
+ var second = jqLite(element.find('optgroup')[1]);
+ var c = jqLite(second.find('option')[0]);
+ var e = jqLite(second.find('option')[1]);
+ expect(second.attr('label')).toEqual('second');
+ expect(c.text()).toEqual('C');
+ expect(e.text()).toEqual('E');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ });
+
+
+ it('should bind to scope value through experession', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item.id as item.name for item in values'
+ });
+
+ scope.$apply(function() {
+ scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+ scope.selected = scope.values[0].id;
+ });
+
+ expect(element.val()).toEqual('0');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[1].id;
+ });
+
+ expect(element.val()).toEqual('1');
+ });
+
+
+ it('should bind to object key', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'key as value for (key, value) in object'
+ });
+
+ scope.$apply(function() {
+ scope.object = {red: 'FF0000', green: '00FF00', blue: '0000FF'};
+ scope.selected = 'green';
+ });
+
+ expect(element.val()).toEqual('green');
+
+ scope.$apply(function() {
+ scope.selected = 'blue';
+ });
+
+ expect(element.val()).toEqual('blue');
+ });
+
+
+ it('should bind to object value', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'value as key for (key, value) in object'
+ });
+
+ scope.$apply(function() {
+ scope.object = {red: 'FF0000', green: '00FF00', blue:'0000FF'};
+ scope.selected = '00FF00';
+ });
+
+ expect(element.val()).toEqual('green');
+
+ scope.$apply(function() {
+ scope.selected = '0000FF';
+ });
+
+ expect(element.val()).toEqual('blue');
+ });
+
+
+ it('should insert a blank option if bound to null', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.val()).toEqual('');
+ expect(jqLite(element.find('option')[0]).val()).toEqual('');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').length).toEqual(1);
+ });
+
+
+ it('should reuse blank option if bound to null', function() {
+ createSingleSelect(true);
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.val()).toEqual('');
+ expect(jqLite(element.find('option')[0]).val()).toEqual('');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').length).toEqual(2);
+ });
+
+
+ it('should insert a unknown option if bound to something not in the list', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = {};
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.val()).toEqual('?');
+ expect(jqLite(element.find('option')[0]).val()).toEqual('?');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').length).toEqual(1);
+ });
+
+
+ it('should select correct input if previously selected option was "?"', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = {};
+ });
+
+ expect(element.find('option').length).toEqual(3);
+ expect(element.val()).toEqual('?');
+ expect(element.find('option').eq(0).val()).toEqual('?');
+
+ browserTrigger(element.find('option').eq(1));
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').eq(0).prop('selected')).toBeTruthy();
+ expect(element.find('option').length).toEqual(2);
+ });
+ });
+
+
+ describe('blank option', function () {
+
+ it('should be compiled as template, be watched and updated', function () {
+ var option;
+ createSingleSelect('<option value="">blank is {{blankVal}}</option>');
+
+ scope.$apply(function() {
+ scope.blankVal = 'so blank';
+ scope.values = [{name: 'A'}];
+ });
+
+ // check blank option is first and is compiled
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
+ expect(option.val()).toBe('');
+ expect(option.text()).toBe('blank is so blank');
+
+ scope.$apply(function() {
+ scope.blankVal = 'not so blank';
+ });
+
+ // check blank option is first and is compiled
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
+ expect(option.val()).toBe('');
+ expect(option.text()).toBe('blank is not so blank');
+ });
+
+
+ it('should support binding via ng-bind-template attribute', function () {
+ var option;
+ createSingleSelect('<option value="" ng-bind-template="blank is {{blankVal}}"></option>');
+
+ scope.$apply(function() {
+ scope.blankVal = 'so blank';
+ scope.values = [{name: 'A'}];
+ });
+
+ // check blank option is first and is compiled
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
+ expect(option.val()).toBe('');
+ expect(option.text()).toBe('blank is so blank');
+ });
+
+
+ it('should support biding via ng-bind attribute', function () {
+ var option;
+ createSingleSelect('<option value="" ng-bind="blankVal"></option>');
+
+ scope.$apply(function() {
+ scope.blankVal = 'is blank';
+ scope.values = [{name: 'A'}];
+ });
+
+ // check blank option is first and is compiled
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
+ expect(option.val()).toBe('');
+ expect(option.text()).toBe('is blank');
+ });
+
+
+ it('should be rendered with the attributes preserved', function () {
+ var option;
+ createSingleSelect('<option value="" class="coyote" id="road-runner" ' +
+ 'custom-attr="custom-attr">{{blankVal}}</option>');
+
+ scope.$apply(function() {
+ scope.blankVal = 'is blank';
+ });
+
+ // check blank option is first and is compiled
+ option = element.find('option').eq(0);
+ expect(option.hasClass('coyote')).toBeTruthy();
+ expect(option.attr('id')).toBe('road-runner');
+ expect(option.attr('custom-attr')).toBe('custom-attr');
+ });
+ });
+
+
+ describe('on change', function() {
+
+ it('should update model on change', function() {
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+
+ element.val('1');
+ browserTrigger(element, 'change');
+ expect(scope.selected).toEqual(scope.values[1]);
+ });
+
+
+ it('should update model on change through expression', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item.id as item.name for item in values'
+ });
+
+ scope.$apply(function() {
+ scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+ scope.selected = scope.values[0].id;
+ });
+
+ expect(element.val()).toEqual('0');
+
+ element.val('1');
+ browserTrigger(element, 'change');
+ expect(scope.selected).toEqual(scope.values[1].id);
+ });
+
+
+ it('should update model to null on change', function() {
+ createSingleSelect(true);
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = scope.values[0];
+ element.val('0');
+ });
+
+ element.val('');
+ browserTrigger(element, 'change');
+ expect(scope.selected).toEqual(null);
+ });
+ });
+
+
+ describe('select-many', function() {
+
+ it('should read multiple selection', function() {
+ createMultiSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = [];
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.find('option')[0].selected).toBeFalsy();
+ expect(element.find('option')[1].selected).toBeFalsy();
+
+ scope.$apply(function() {
+ scope.selected.push(scope.values[1]);
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.find('option')[0].selected).toBeFalsy();
+ expect(element.find('option')[1].selected).toBeTruthy();
+
+ scope.$apply(function() {
+ scope.selected.push(scope.values[0]);
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.find('option')[0].selected).toBeTruthy();
+ expect(element.find('option')[1].selected).toBeTruthy();
+ });
+
+
+ it('should update model on change', function() {
+ createMultiSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = [];
+ });
+
+ element.find('option')[0].selected = true;
+
+ browserTrigger(element, 'change');
+ expect(scope.selected).toEqual([scope.values[0]]);
+ });
+
+ it('should select from object', function() {
+ createSelect({
+ 'ng-model':'selected',
+ 'multiple':true,
+ 'ng-options':'key as value for (key,value) in values'
+ });
+ scope.values = {'0':'A', '1':'B'};
+
+ scope.selected = ['1'];
+ scope.$digest();
+ expect(element.find('option')[1].selected).toBe(true);
+
+ element.find('option')[0].selected = true;
+ browserTrigger(element, 'change');
+ expect(scope.selected).toEqual(['0', '1']);
+
+ element.find('option')[1].selected = false;
+ browserTrigger(element, 'change');
+ expect(scope.selected).toEqual(['0']);
+ });
+ });
+
+
+ describe('ng-required', function() {
+
+ it('should allow bindings on ng-required', function() {
+ createSelect({
+ 'ng-model': 'value',
+ 'ng-options': 'item.name for item in values',
+ 'ng-required': 'required'
+ }, true);
+
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}];
+ scope.required = false;
+ });
+
+ element.val('');
+ browserTrigger(element, 'change');
+ expect(element).toBeValid();
+
+ scope.$apply(function() {
+ scope.required = true;
+ });
+ expect(element).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.value = scope.values[0];
+ });
+ expect(element).toBeValid();
+
+ element.val('');
+ browserTrigger(element, 'change');
+ expect(element).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.required = false;
+ });
+ expect(element).toBeValid();
+ });
+ });
+ });
+
+
+ describe('OPTION value', function() {
+ beforeEach(function() {
+ this.addMatchers({
+ toHaveValue: function(expected){
+ this.message = function() {
+ return 'Expected "' + this.actual.html() + '" to have value="' + expected + '".';
+ };
+
+ var value;
+ htmlParser(this.actual.html(), {
+ start:function(tag, attrs){
+ value = attrs.value;
+ },
+ end:noop,
+ chars:noop
+ });
+ return trim(value) == trim(expected);
+ }
+ });
+ });
+
+
+ it('should populate value attribute on OPTION', inject(function($rootScope, $compile) {
+ element = $compile('<select ng-model="x"><option>abc</option></select>')($rootScope)
+ expect(element).toHaveValue('abc');
+ }));
+
+ it('should ignore value if already exists', inject(function($rootScope, $compile) {
+ element = $compile('<select ng-model="x"><option value="abc">xyz</option></select>')($rootScope)
+ expect(element).toHaveValue('abc');
+ }));
+
+ it('should set value even if newlines present', inject(function($rootScope, $compile) {
+ element = $compile('<select ng-model="x"><option attr="\ntext\n" \n>\nabc\n</option></select>')($rootScope)
+ expect(element).toHaveValue('\nabc\n');
+ }));
+
+ it('should set value even if self closing HTML', inject(function($rootScope, $compile) {
+ // IE removes the \n from option, which makes this test pointless
+ if (msie) return;
+ element = $compile('<select ng-model="x"><option>\n</option></select>')($rootScope)
+ expect(element).toHaveValue('\n');
+ }));
+ });
+});
diff --git a/test/ng/directive/styleSpec.js b/test/ng/directive/styleSpec.js
new file mode 100644
index 00000000..bdc4ea85
--- /dev/null
+++ b/test/ng/directive/styleSpec.js
@@ -0,0 +1,31 @@
+'use strict';
+
+describe('style', function() {
+ var element;
+
+
+ afterEach(function() {
+ dealoc(element);
+ });
+
+
+ it('should not compile style element', inject(function($compile, $rootScope) {
+ element = jqLite('<style type="text/css">should {{notBound}}</style>');
+ $compile(element)($rootScope);
+ $rootScope.$digest();
+
+ // read innerHTML and trim to pass on IE8
+ expect(trim(element[0].innerHTML)).toBe('should {{notBound}}');
+ }));
+
+
+ it('should compile content of element with style attr', inject(function($compile, $rootScope) {
+ element = jqLite('<div style="some">{{bind}}</div>');
+ $compile(element)($rootScope);
+ $rootScope.$apply(function() {
+ $rootScope.bind = 'value';
+ });
+
+ expect(element.text()).toBe('value');
+ }));
+});
diff --git a/test/ng/documentSpec.js b/test/ng/documentSpec.js
new file mode 100644
index 00000000..064904a2
--- /dev/null
+++ b/test/ng/documentSpec.js
@@ -0,0 +1,9 @@
+'use strict';
+
+describe('$document', function() {
+
+
+ it("should inject $document", inject(function($document) {
+ expect($document).toEqual(jqLite(document));
+ }));
+});
diff --git a/test/ng/exceptionHandlerSpec.js b/test/ng/exceptionHandlerSpec.js
new file mode 100644
index 00000000..3f3dd800
--- /dev/null
+++ b/test/ng/exceptionHandlerSpec.js
@@ -0,0 +1,24 @@
+'use strict';
+
+describe('$exceptionHandler', function() {
+ it('should log errors with single argument', function() {
+ module(function($provide){
+ $provide.provider('$exceptionHandler', $ExceptionHandlerProvider);
+ });
+ inject(function($log, $exceptionHandler) {
+ $exceptionHandler('myError');
+ expect($log.error.logs.shift()).toEqual(['myError']);
+ });
+ });
+
+
+ it('should log errors with multiple arguments', function() {
+ module(function($provide){
+ $provide.provider('$exceptionHandler', $ExceptionHandlerProvider);
+ });
+ inject(function($log, $exceptionHandler) {
+ $exceptionHandler('myError', 'comment');
+ expect($log.error.logs.shift()).toEqual(['myError', 'comment']);
+ });
+ });
+});
diff --git a/test/ng/filter/filterSpec.js b/test/ng/filter/filterSpec.js
new file mode 100644
index 00000000..a33358d0
--- /dev/null
+++ b/test/ng/filter/filterSpec.js
@@ -0,0 +1,69 @@
+'use strict';
+
+describe('Filter: filter', function() {
+ var filter;
+
+ beforeEach(inject(function($filter){
+ filter = $filter('filter');
+ }));
+
+ it('should filter by string', function() {
+ var items = ['MIsKO', {name: 'shyam'}, ['adam'], 1234];
+ expect(filter(items, '').length).toBe(4);
+ expect(filter(items, undefined).length).toBe(4);
+
+ expect(filter(items, 'iSk').length).toBe(1);
+ expect(filter(items, 'isk')[0]).toBe('MIsKO');
+
+ expect(filter(items, 'yam').length).toBe(1);
+ expect(filter(items, 'yam')[0]).toEqual(items[1]);
+
+ expect(filter(items, 'da').length).toBe(1);
+ expect(filter(items, 'da')[0]).toEqual(items[2]);
+
+ expect(filter(items, '34').length).toBe(1);
+ expect(filter(items, '34')[0]).toBe(1234);
+
+ expect(filter(items, "I don't exist").length).toBe(0);
+ });
+
+ it('should not read $ properties', function() {
+ expect(''.charAt(0)).toBe(''); // assumption
+
+ var items = [{$name: 'misko'}];
+ expect(filter(items, 'misko').length).toBe(0);
+ });
+
+ it('should filter on specific property', function() {
+ var items = [{ignore: 'a', name: 'a'}, {ignore: 'a', name: 'abc'}];
+ expect(filter(items, {}).length).toBe(2);
+
+ expect(filter(items, {name: 'a'}).length).toBe(2);
+
+ expect(filter(items, {name: 'b'}).length).toBe(1);
+ expect(filter(items, {name: 'b'})[0].name).toBe('abc');
+ });
+
+ it('should take function as predicate', function() {
+ var items = [{name: 'a'}, {name: 'abc', done: true}];
+ expect(filter(items, function(i) {return i.done;}).length).toBe(1);
+ });
+
+ it('should take object as perdicate', function() {
+ var items = [{first: 'misko', last: 'hevery'},
+ {first: 'adam', last: 'abrons'}];
+
+ expect(filter(items, {first:'', last:''}).length).toBe(2);
+ expect(filter(items, {first:'', last:'hevery'}).length).toBe(1);
+ expect(filter(items, {first:'adam', last:'hevery'}).length).toBe(0);
+ expect(filter(items, {first:'misko', last:'hevery'}).length).toBe(1);
+ expect(filter(items, {first:'misko', last:'hevery'})[0]).toEqual(items[0]);
+ });
+
+ it('should support negation operator', function() {
+ var items = ['misko', 'adam'];
+
+ expect(filter(items, '!isk').length).toBe(1);
+ expect(filter(items, '!isk')[0]).toEqual(items[1]);
+ });
+});
diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js
new file mode 100644
index 00000000..98651c58
--- /dev/null
+++ b/test/ng/filter/filtersSpec.js
@@ -0,0 +1,280 @@
+'use strict';
+
+describe('filters', function() {
+
+ var filter;
+
+ beforeEach(inject(function($filter){
+ filter = $filter;
+ }));
+
+ it('should call the filter when evaluating expression', function(){
+ var filter = jasmine.createSpy('myFilter');
+ createInjector(['ng', function($filterProvider) {
+ $filterProvider.register('myFilter', valueFn(filter));
+ }]).invoke(function($rootScope) {
+ $rootScope.$eval('10|myFilter');
+ });
+ expect(filter).toHaveBeenCalledWith(10);
+ });
+
+ describe('formatNumber', function() {
+ var pattern;
+
+ beforeEach(function() {
+ pattern = { minInt: 1,
+ minFrac: 0,
+ maxFrac: 3,
+ posPre: '',
+ posSuf: '',
+ negPre: '-',
+ negSuf: '',
+ gSize: 3,
+ lgSize: 3 };
+ });
+
+ it('should format according to different patterns', function() {
+ pattern.gSize = 2;
+ var num = formatNumber(1234567.89, pattern, ',', '.');
+ expect(num).toBe('12,34,567.89');
+ num = formatNumber(1234.56, pattern, ',', '.');
+ expect(num).toBe('1,234.56');
+
+ pattern.negPre = '(';
+ pattern.negSuf = '-)';
+ num = formatNumber(-1234, pattern, ',', '.');
+ expect(num).toBe('(1,234-)');
+ pattern.posPre = '+';
+ pattern.posSuf = '+';
+ num = formatNumber(1234, pattern, ',', '.');
+ expect(num).toBe('+1,234+');
+ pattern.posPre = pattern.posSuf = '';
+
+ pattern.minFrac = 2;
+ num = formatNumber(1, pattern, ',', '.');
+ expect(num).toBe('1.00');
+ pattern.maxFrac = 4;
+ num = formatNumber(1.11119, pattern, ',', '.');
+ expect(num).toBe('1.1112');
+ });
+
+ it('should format according different seperators', function() {
+ var num = formatNumber(1234567.1, pattern, '.', ',', 2);
+ expect(num).toBe('1.234.567,10');
+ });
+
+ it('should format with or without fractionSize', function() {
+ var num = formatNumber(123.1, pattern, ',', '.', 3);
+ expect(num).toBe('123.100');
+ num = formatNumber(123.12, pattern, ',', '.');
+ expect(num).toBe('123.12');
+ var num = formatNumber(123.1116, pattern, ',', '.');
+ expect(num).toBe('123.112');
+ });
+ });
+
+ describe('currency', function() {
+ var currency;
+
+ beforeEach(function() {
+ currency = filter('currency');
+ });
+
+ it('should do basic currency filtering', function() {
+ expect(currency(0)).toEqual('$0.00');
+ expect(currency(-999)).toEqual('($999.00)');
+ expect(currency(1234.5678, "USD$")).toEqual('USD$1,234.57');
+ });
+
+
+ it('should return empty string for non-numbers', function() {
+ expect(currency()).toBe('');
+ expect(currency('abc')).toBe('');
+ });
+ });
+
+
+ describe('number', function() {
+ var number;
+
+ beforeEach(inject(function($rootScope) {
+ number = filter('number');
+ }));
+
+
+ it('should do basic filter', function() {
+ expect(number(0, 0)).toEqual('0');
+ expect(number(-999)).toEqual('-999');
+ expect(number(123)).toEqual('123');
+ expect(number(1234567)).toEqual('1,234,567');
+ expect(number(1234)).toEqual('1,234');
+ expect(number(1234.5678)).toEqual('1,234.568');
+ expect(number(Number.NaN)).toEqual('');
+ expect(number("1234.5678")).toEqual('1,234.568');
+ expect(number(1/0)).toEqual("");
+ expect(number(1, 2)).toEqual("1.00");
+ expect(number(.1, 2)).toEqual("0.10");
+ expect(number(.01, 2)).toEqual("0.01");
+ expect(number(.001, 3)).toEqual("0.001");
+ expect(number(.0001, 3)).toEqual("0.000");
+ expect(number(9, 2)).toEqual("9.00");
+ expect(number(.9, 2)).toEqual("0.90");
+ expect(number(.99, 2)).toEqual("0.99");
+ expect(number(.999, 3)).toEqual("0.999");
+ expect(number(.9999, 3)).toEqual("1.000");
+ expect(number(1234.567, 0)).toEqual("1,235");
+ expect(number(1234.567, 1)).toEqual("1,234.6");
+ expect(number(1234.567, 2)).toEqual("1,234.57");
+ });
+
+ it('should filter exponential numbers', function() {
+ expect(number(1e50, 0)).toEqual('1e+50');
+ expect(number(-2e50, 2)).toEqual('-2e+50');
+ });
+ });
+
+ describe('json', function () {
+ it('should do basic filter', function() {
+ expect(filter('json')({a:"b"})).toEqual(toJson({a:"b"}, true));
+ });
+ });
+
+ describe('lowercase', function() {
+ it('should do basic filter', function() {
+ expect(filter('lowercase')('AbC')).toEqual('abc');
+ expect(filter('lowercase')(null)).toBeNull();
+ });
+ });
+
+ describe('uppercase', function() {
+ it('should do basic filter', function() {
+ expect(filter('uppercase')('AbC')).toEqual('ABC');
+ expect(filter('uppercase')(null)).toBeNull();
+ });
+ });
+
+ describe('linky', function() {
+ var linky;
+
+ beforeEach(inject(function($filter){
+ linky = $filter('linky')
+ }));
+
+ it('should do basic filter', function() {
+ expect(linky("http://ab/ (http://a/) <http://a/> http://1.2/v:~-123. c")).
+ toEqual('<a href="http://ab/">http://ab/</a> ' +
+ '(<a href="http://a/">http://a/</a>) ' +
+ '&lt;<a href="http://a/">http://a/</a>&gt; ' +
+ '<a href="http://1.2/v:~-123">http://1.2/v:~-123</a>. c');
+ expect(linky(undefined)).not.toBeDefined();
+ });
+
+ it('should handle mailto:', function() {
+ expect(linky("mailto:me@example.com")).
+ toEqual('<a href="mailto:me@example.com">me@example.com</a>');
+ expect(linky("me@example.com")).
+ toEqual('<a href="mailto:me@example.com">me@example.com</a>');
+ expect(linky("send email to me@example.com, but")).
+ toEqual('send email to <a href="mailto:me@example.com">me@example.com</a>, but');
+ });
+ });
+
+ describe('date', function() {
+
+ var morning = new angular.mock.TzDate(+5, '2010-09-03T12:05:08.000Z'); //7am
+ var noon = new angular.mock.TzDate(+5, '2010-09-03T17:05:08.000Z'); //12pm
+ var midnight = new angular.mock.TzDate(+5, '2010-09-03T05:05:08.000Z'); //12am
+ var earlyDate = new angular.mock.TzDate(+5, '0001-09-03T05:05:08.000Z');
+
+ var date;
+
+ beforeEach(inject(function($filter) {
+ date = $filter('date');
+ }));
+
+ it('should ignore falsy inputs', function() {
+ expect(date(null)).toBeNull();
+ expect(date('')).toEqual('');
+ });
+
+ it('should do basic filter', function() {
+ expect(date(noon)).toEqual(date(noon, 'mediumDate'));
+ expect(date(noon, '')).toEqual(date(noon, 'mediumDate'));
+ });
+
+ it('should accept number or number string representing milliseconds as input', function() {
+ expect(date(noon.getTime())).toEqual(date(noon.getTime(), 'mediumDate'));
+ expect(date(noon.getTime() + "")).toEqual(date(noon.getTime() + "", 'mediumDate'));
+ });
+
+ it('should accept various format strings', function() {
+ expect(date(morning, "yy-MM-dd HH:mm:ss")).
+ toEqual('10-09-03 07:05:08');
+
+ expect(date(midnight, "yyyy-M-d h=H:m:saZ")).
+ toEqual('2010-9-3 12=0:5:8AM0500');
+
+ expect(date(midnight, "yyyy-MM-dd hh=HH:mm:ssaZ")).
+ toEqual('2010-09-03 12=00:05:08AM0500');
+
+ expect(date(noon, "yyyy-MM-dd hh=HH:mm:ssaZ")).
+ toEqual('2010-09-03 12=12:05:08PM0500');
+
+ expect(date(noon, "EEE, MMM d, yyyy")).
+ toEqual('Fri, Sep 3, 2010');
+
+ expect(date(noon, "EEEE, MMMM dd, yyyy")).
+ toEqual('Friday, September 03, 2010');
+
+ expect(date(earlyDate, "MMMM dd, y")).
+ toEqual('September 03, 1');
+ });
+
+ it('should treat single quoted strings as string literals', function() {
+ expect(date(midnight, "yyyy'de' 'a'x'dd' 'adZ' h=H:m:saZ")).
+ toEqual('2010de axdd adZ 12=0:5:8AM0500');
+ });
+
+ it('should treat a sequence of two single quotes as a literal single quote', function() {
+ expect(date(midnight, "yyyy'de' 'a''dd' 'adZ' h=H:m:saZ")).
+ toEqual("2010de a'dd adZ 12=0:5:8AM0500");
+ });
+
+ it('should accept default formats', function() {
+
+ expect(date(noon, "medium")).
+ toEqual('Sep 3, 2010 12:05:08 PM');
+
+ expect(date(noon, "short")).
+ toEqual('9/3/10 12:05 PM');
+
+ expect(date(noon, "fullDate")).
+ toEqual('Friday, September 3, 2010');
+
+ expect(date(noon, "longDate")).
+ toEqual('September 3, 2010');
+
+ expect(date(noon, "mediumDate")).
+ toEqual('Sep 3, 2010');
+
+ expect(date(noon, "shortDate")).
+ toEqual('9/3/10');
+
+ expect(date(noon, "mediumTime")).
+ toEqual('12:05:08 PM');
+
+ expect(date(noon, "shortTime")).
+ toEqual('12:05 PM');
+ });
+
+ it('should be able to parse ISO 8601 dates/times using', function() {
+ var isoString = '2010-09-03T05:05:08.872Z';
+ expect(date(isoString)).
+ toEqual(date(isoString, 'mediumDate'));
+ });
+
+ it('should parse format ending with non-replaced string', function() {
+ expect(date(morning, 'yy/xxx')).toEqual('10/xxx');
+ });
+ });
+});
diff --git a/test/ng/filter/limitToSpec.js b/test/ng/filter/limitToSpec.js
new file mode 100644
index 00000000..b0977235
--- /dev/null
+++ b/test/ng/filter/limitToSpec.js
@@ -0,0 +1,52 @@
+'use strict';
+
+describe('Filter: limitTo', function() {
+ var items;
+ var limitTo;
+
+ beforeEach(inject(function($filter) {
+ items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
+ limitTo = $filter('limitTo');
+ }));
+
+
+ it('should return the first X items when X is positive', function() {
+ expect(limitTo(items, 3)).toEqual(['a', 'b', 'c']);
+ expect(limitTo(items, '3')).toEqual(['a', 'b', 'c']);
+ });
+
+
+ it('should return the last X items when X is negative', function() {
+ expect(limitTo(items, -3)).toEqual(['f', 'g', 'h']);
+ expect(limitTo(items, '-3')).toEqual(['f', 'g', 'h']);
+ });
+
+
+ it('should return an empty array when X cannot be parsed', function() {
+ expect(limitTo(items, 'bogus')).toEqual([]);
+ expect(limitTo(items, 'null')).toEqual([]);
+ expect(limitTo(items, 'undefined')).toEqual([]);
+ expect(limitTo(items, null)).toEqual([]);
+ expect(limitTo(items, undefined)).toEqual([]);
+ });
+
+
+ it('should return an empty array when input is not Array type', function() {
+ expect(limitTo('bogus', 1)).toEqual('bogus');
+ expect(limitTo(null, 1)).toEqual(null);
+ expect(limitTo(undefined, 1)).toEqual(undefined);
+ expect(limitTo(null, 1)).toEqual(null);
+ expect(limitTo(undefined, 1)).toEqual(undefined);
+ expect(limitTo({}, 1)).toEqual({});
+ });
+
+
+ it('should return a copy of input array if X is exceeds array length', function () {
+ expect(limitTo(items, 19)).toEqual(items);
+ expect(limitTo(items, '9')).toEqual(items);
+ expect(limitTo(items, -9)).toEqual(items);
+ expect(limitTo(items, '-9')).toEqual(items);
+
+ expect(limitTo(items, 9)).not.toBe(items);
+ });
+});
diff --git a/test/ng/filter/orderBySpec.js b/test/ng/filter/orderBySpec.js
new file mode 100644
index 00000000..5c117891
--- /dev/null
+++ b/test/ng/filter/orderBySpec.js
@@ -0,0 +1,34 @@
+'use strict';
+
+describe('Filter: orderBy', function() {
+ var orderBy;
+ beforeEach(inject(function($filter) {
+ orderBy = $filter('orderBy');
+ }));
+
+ it('should return same array if predicate is falsy', function() {
+ var array = [1, 2, 3];
+ expect(orderBy(array)).toBe(array);
+ });
+
+ it('shouldSortArrayInReverse', function() {
+ expect(orderBy([{a:15}, {a:2}], 'a', true)).toEqualData([{a:15}, {a:2}]);
+ expect(orderBy([{a:15}, {a:2}], 'a', "T")).toEqualData([{a:15}, {a:2}]);
+ expect(orderBy([{a:15}, {a:2}], 'a', "reverse")).toEqualData([{a:15}, {a:2}]);
+ });
+
+ it('should sort array by predicate', function() {
+ expect(orderBy([{a:15, b:1}, {a:2, b:1}], ['a', 'b'])).toEqualData([{a:2, b:1}, {a:15, b:1}]);
+ expect(orderBy([{a:15, b:1}, {a:2, b:1}], ['b', 'a'])).toEqualData([{a:2, b:1}, {a:15, b:1}]);
+ expect(orderBy([{a:15, b:1}, {a:2, b:1}], ['+b', '-a'])).toEqualData([{a:15, b:1}, {a:2, b:1}]);
+ });
+
+ it('should use function', function() {
+ expect(
+ orderBy(
+ [{a:15, b:1},{a:2, b:1}],
+ function(value) { return value.a; })).
+ toEqual([{a:2, b:1},{a:15, b:1}]);
+ });
+
+});
diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js
new file mode 100644
index 00000000..820099e8
--- /dev/null
+++ b/test/ng/httpBackendSpec.js
@@ -0,0 +1,241 @@
+describe('$httpBackend', function() {
+
+ var $backend, $browser, callbacks,
+ xhr, fakeBody, callback;
+
+ // TODO(vojta): should be replaced by $defer mock
+ function fakeTimeout(fn, delay) {
+ fakeTimeout.fns.push(fn);
+ fakeTimeout.delays.push(delay);
+ }
+
+ fakeTimeout.fns = [];
+ fakeTimeout.delays = [];
+ fakeTimeout.flush = function() {
+ var len = fakeTimeout.fns.length;
+ fakeTimeout.delays = [];
+ while (len--) fakeTimeout.fns.shift()();
+ };
+
+
+ beforeEach(inject(function($injector) {
+ callbacks = {counter: 0};
+ $browser = $injector.get('$browser');
+ fakeBody = {removeChild: jasmine.createSpy('body.removeChild')};
+ $backend = createHttpBackend($browser, MockXhr, fakeTimeout, callbacks, fakeBody);
+ 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 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();
+ });
+
+
+ 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 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 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, SyncXhr);
+ $backend('GET', '/url', null, callback);
+ 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($browser.$$scripts.length).toBe(1);
+
+ var script = $browser.$$scripts.shift(),
+ url = script.url.match(SCRIPT_URL);
+
+ expect(url[1]).toBe('http://example.org/path');
+ callbacks[url[2]]('some-data');
+ script.done();
+
+ 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($browser.$$scripts.length).toBe(1);
+
+ var script = $browser.$$scripts.shift(),
+ callbackId = script.url.match(SCRIPT_URL)[2];
+
+ callbacks[callbackId]('some-data');
+ script.done();
+
+ expect(callbacks[callbackId]).toBeUndefined();
+ expect(fakeBody.removeChild).toHaveBeenCalledOnce();
+ expect(fakeBody.removeChild).toHaveBeenCalledWith(script);
+ });
+
+
+ it('should call callback with status -2 when script fails to load', function() {
+ callback.andCallFake(function(status, response) {
+ expect(status).toBe(-2);
+ expect(response).toBeUndefined();
+ });
+
+ $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
+ expect($browser.$$scripts.length).toBe(1);
+
+ $browser.$$scripts.shift().done();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should set url to current location if not specified or empty string', function() {
+ $backend('JSONP', undefined, null, callback);
+ expect($browser.$$scripts[0].url).toBe($browser.url());
+ $browser.$$scripts.shift();
+
+ $backend('JSONP', '', null, callback);
+ expect($browser.$$scripts[0].url).toBe($browser.url());
+ $browser.$$scripts.shift();
+ });
+
+
+ // TODO(vojta): test whether it fires "async-start"
+ // TODO(vojta): test whether it fires "async-end" on both success and error
+ });
+
+ describe('file protocol', 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', function() {
+ $backend = createHttpBackend($browser, MockXhr, null, null, null, 'http');
+
+ $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 - relative url', function() {
+ $backend = createHttpBackend($browser, MockXhr, null, null, null, 'file');
+
+ $backend('GET', '/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', function() {
+ $backend = createHttpBackend($browser, MockXhr, null, null, null, 'http');
+
+ $backend('GET', 'file:///whatever/index.html', null, callback);
+ respond(0, '');
+
+ expect(callback).toHaveBeenCalled();
+ expect(callback.mostRecentCall.args[0]).toBe(404);
+ });
+
+
+ it('should convert 0 to 200 if content - relative url', function() {
+ $backend = createHttpBackend($browser, MockXhr, null, null, null, 'file');
+
+ $backend('GET', '/whatever/index.html', null, callback);
+ respond(0, '');
+
+ expect(callback).toHaveBeenCalled();
+ expect(callback.mostRecentCall.args[0]).toBe(404);
+ });
+ });
+});
+
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
new file mode 100644
index 00000000..ab50827c
--- /dev/null
+++ b/test/ng/httpSpec.js
@@ -0,0 +1,946 @@
+'use strict';
+
+describe('$http', function() {
+
+ var callback;
+
+ beforeEach(function() {
+ callback = jasmine.createSpy('done');
+ });
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ }));
+
+ afterEach(inject(function($exceptionHandler, $httpBackend) {
+ forEach($exceptionHandler.errors, function(e) {
+ dump('Unhandled exception: ', e)
+ });
+
+ if ($exceptionHandler.errors.length) {
+ throw 'Unhandled exceptions trapped in $exceptionHandler!';
+ }
+
+ $httpBackend.verifyNoOutstandingExpectation();
+ }));
+
+
+ describe('$httpProvider', function() {
+
+ describe('interceptors', function() {
+
+ it('should default to an empty array', module(function($httpProvider) {
+ expect($httpProvider.responseInterceptors).toEqual([]);
+ }));
+
+
+ it('should pass the responses through interceptors', function() {
+ module(function($httpProvider, $provide) {
+ $provide.factory('testInterceptor', function ($q) {
+ return function(httpPromise) {
+ return httpPromise.then(function(response) {
+ var deferred = $q.defer();
+ deferred.resolve({
+ data: response.data + '?',
+ status: 209,
+ headers: response.headers,
+ config: response.config
+ });
+ return deferred.promise;
+ });
+ };
+ });
+ // just change the response data and pass the response object along
+ $httpProvider.responseInterceptors.push(function() {
+ return function(httpPromise) {
+ return httpPromise.then(function(response) {
+ response.data += '!';
+ return response;
+ });
+ }
+ });
+
+ // return a new resolved promise representing modified response object
+ $httpProvider.responseInterceptors.push('testInterceptor');
+ });
+ inject(function($http, $httpBackend) {
+ $httpBackend.expect('GET', '/foo').respond(201, 'Hello');
+ $http.get('/foo').success(function(data, status) {
+ expect(data).toBe('Hello!?');
+ expect(status).toBe(209);
+ callback();
+ });
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+ });
+
+
+ it('should support interceptors defined as services', function() {
+ module(function($provide, $httpProvider) {
+ $provide.factory('myInterceptor', function() {
+ return function(promise) {
+ return promise.then(function(response) {
+ response.data = uppercase(response.data);
+ return response;
+ });
+ }
+ });
+ $httpProvider.responseInterceptors.push('myInterceptor');
+ });
+ inject(function($http, $httpBackend) {
+ var response;
+
+ $httpBackend.expect('GET', '/test').respond('hello!');
+ $http.get('/test').success(function(data) {response = data;});
+ expect(response).toBeUndefined();
+
+ $httpBackend.flush();
+ expect(response).toBe('HELLO!');
+ });
+ });
+ });
+ });
+
+
+ describe('the instance', function() {
+ var $httpBackend, $http, $rootScope;
+
+ beforeEach(inject(['$rootScope', function($rs) {
+ $rootScope = $rs;
+
+ spyOn($rootScope, '$apply').andCallThrough();
+ }]));
+
+ beforeEach(inject(['$httpBackend', '$http', function($hb, $h) {
+ $httpBackend = $hb;
+ $http = $h;
+ }]));
+
+
+ it('should do basic request', inject(function($httpBackend, $http) {
+ $httpBackend.expect('GET', '/url').respond('');
+ $http({url: '/url', method: 'GET'});
+ }));
+
+
+ it('should pass data if specified', inject(function($httpBackend, $http) {
+ $httpBackend.expect('POST', '/url', 'some-data').respond('');
+ $http({url: '/url', method: 'POST', data: 'some-data'});
+ }));
+
+
+ // TODO(vojta): test passing timeout
+
+
+ describe('params', function() {
+ it('should do basic request with params and encode', inject(function($httpBackend, $http) {
+ $httpBackend.expect('GET', '/url?a%3D=%3F%26&b=2').respond('');
+ $http({url: '/url', params: {'a=':'?&', b:2}, method: 'GET'});
+ }));
+
+
+ it('should merge params if url contains some already', inject(function($httpBackend, $http) {
+ $httpBackend.expect('GET', '/url?c=3&a=1&b=2').respond('');
+ $http({url: '/url?c=3', params: {a:1, b:2}, method: 'GET'});
+ }));
+
+
+ it('should jsonify objects in params map', inject(function($httpBackend, $http) {
+ $httpBackend.expect('GET', '/url?a=1&b=%7B%22c%22%3A3%7D').respond('');
+ $http({url: '/url', params: {a:1, b:{c:3}}, method: 'GET'});
+ }));
+ });
+
+
+ describe('callbacks', function() {
+
+ it('should pass in the response object when a request is successful', function() {
+ $httpBackend.expect('GET', '/url').respond(207, 'my content', {'content-encoding': 'smurf'});
+ $http({url: '/url', method: 'GET'}).then(function(response) {
+ expect(response.data).toBe('my content');
+ expect(response.status).toBe(207);
+ expect(response.headers()).toEqual({'content-encoding': 'smurf'});
+ expect(response.config.url).toBe('/url');
+ callback();
+ });
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should pass in the response object when a request failed', function() {
+ $httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'});
+ $http({url: '/url', method: 'GET'}).then(null, function(response) {
+ expect(response.data).toBe('bad error');
+ expect(response.status).toBe(543);
+ expect(response.headers()).toEqual({'request-id': '123'});
+ expect(response.config.url).toBe('/url');
+ callback();
+ });
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ describe('success', function() {
+ it('should allow http specific callbacks to be registered via "success"', function() {
+ $httpBackend.expect('GET', '/url').respond(207, 'my content', {'content-encoding': 'smurf'});
+ $http({url: '/url', method: 'GET'}).success(function(data, status, headers, config) {
+ expect(data).toBe('my content');
+ expect(status).toBe(207);
+ expect(headers()).toEqual({'content-encoding': 'smurf'});
+ expect(config.url).toBe('/url');
+ callback();
+ });
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should return the original http promise', function() {
+ $httpBackend.expect('GET', '/url').respond(207, 'my content', {'content-encoding': 'smurf'});
+ var httpPromise = $http({url: '/url', method: 'GET'});
+ expect(httpPromise.success(callback)).toBe(httpPromise);
+ });
+ });
+
+
+ describe('error', function() {
+ it('should allow http specific callbacks to be registered via "error"', function() {
+ $httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'});
+ $http({url: '/url', method: 'GET'}).error(function(data, status, headers, config) {
+ expect(data).toBe('bad error');
+ expect(status).toBe(543);
+ expect(headers()).toEqual({'request-id': '123'});
+ expect(config.url).toBe('/url');
+ callback();
+ });
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should return the original http promise', function() {
+ $httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'});
+ var httpPromise = $http({url: '/url', method: 'GET'});
+ expect(httpPromise.error(callback)).toBe(httpPromise);
+ });
+ });
+ });
+
+
+ describe('response headers', function() {
+
+ it('should return single header', function() {
+ $httpBackend.expect('GET', '/url').respond('', {'date': 'date-val'});
+ callback.andCallFake(function(r) {
+ expect(r.headers('date')).toBe('date-val');
+ });
+
+ $http({url: '/url', method: 'GET'}).then(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should return null when single header does not exist', function() {
+ $httpBackend.expect('GET', '/url').respond('', {'Some-Header': 'Fake'});
+ callback.andCallFake(function(r) {
+ r.headers(); // we need that to get headers parsed first
+ expect(r.headers('nothing')).toBe(null);
+ });
+
+ $http({url: '/url', method: 'GET'}).then(callback)
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should return all headers as object', function() {
+ $httpBackend.expect('GET', '/url').respond('', {
+ 'content-encoding': 'gzip',
+ 'server': 'Apache'
+ });
+
+ callback.andCallFake(function(r) {
+ expect(r.headers()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'});
+ });
+
+ $http({url: '/url', method: 'GET'}).then(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should return empty object for jsonp request', function() {
+ callback.andCallFake(function(r) {
+ expect(r.headers()).toEqual({});
+ });
+
+ $httpBackend.expect('JSONP', '/some').respond(200);
+ $http({url: '/some', method: 'JSONP'}).then(callback);
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+ });
+
+
+ describe('response headers parser', function() {
+
+ it('should parse basic', function() {
+ var parsed = parseHeaders(
+ 'date: Thu, 04 Aug 2011 20:23:08 GMT\n' +
+ 'content-encoding: gzip\n' +
+ 'transfer-encoding: chunked\n' +
+ 'x-cache-info: not cacheable; response has already expired, not cacheable; response has already expired\n' +
+ 'connection: Keep-Alive\n' +
+ 'x-backend-server: pm-dekiwiki03\n' +
+ 'pragma: no-cache\n' +
+ 'server: Apache\n' +
+ 'x-frame-options: DENY\n' +
+ 'content-type: text/html; charset=utf-8\n' +
+ 'vary: Cookie, Accept-Encoding\n' +
+ 'keep-alive: timeout=5, max=1000\n' +
+ 'expires: Thu: , 19 Nov 1981 08:52:00 GMT\n');
+
+ expect(parsed['date']).toBe('Thu, 04 Aug 2011 20:23:08 GMT');
+ expect(parsed['content-encoding']).toBe('gzip');
+ expect(parsed['transfer-encoding']).toBe('chunked');
+ expect(parsed['keep-alive']).toBe('timeout=5, max=1000');
+ });
+
+
+ it('should parse lines without space after colon', function() {
+ expect(parseHeaders('key:value').key).toBe('value');
+ });
+
+
+ it('should trim the values', function() {
+ expect(parseHeaders('key: value ').key).toBe('value');
+ });
+
+
+ it('should allow headers without value', function() {
+ expect(parseHeaders('key:').key).toBe('');
+ });
+
+
+ it('should merge headers with same key', function() {
+ expect(parseHeaders('key: a\nkey:b\n').key).toBe('a, b');
+ });
+
+
+ it('should normalize keys to lower case', function() {
+ expect(parseHeaders('KeY: value').key).toBe('value');
+ });
+
+
+ it('should parse CRLF as delimiter', function() {
+ // IE does use CRLF
+ expect(parseHeaders('a: b\r\nc: d\r\n')).toEqual({a: 'b', c: 'd'});
+ expect(parseHeaders('a: b\r\nc: d\r\n').a).toBe('b');
+ });
+
+
+ it('should parse tab after semi-colon', function() {
+ expect(parseHeaders('a:\tbb').a).toBe('bb');
+ expect(parseHeaders('a: \tbb').a).toBe('bb');
+ });
+ });
+
+
+ describe('request headers', function() {
+
+ it('should send custom headers', function() {
+ $httpBackend.expect('GET', '/url', undefined, function(headers) {
+ return headers['Custom'] == 'header';
+ }).respond('');
+
+ $http({url: '/url', method: 'GET', headers: {
+ 'Custom': 'header',
+ }});
+
+ $httpBackend.flush();
+ });
+
+
+ it('should set default headers for GET request', function() {
+ $httpBackend.expect('GET', '/url', undefined, function(headers) {
+ return headers['Accept'] == 'application/json, text/plain, */*' &&
+ headers['X-Requested-With'] == 'XMLHttpRequest';
+ }).respond('');
+
+ $http({url: '/url', method: 'GET', headers: {}});
+ $httpBackend.flush();
+ });
+
+
+ it('should set default headers for POST request', function() {
+ $httpBackend.expect('POST', '/url', 'messageBody', function(headers) {
+ return headers['Accept'] == 'application/json, text/plain, */*' &&
+ headers['X-Requested-With'] == 'XMLHttpRequest' &&
+ headers['Content-Type'] == 'application/json';
+ }).respond('');
+
+ $http({url: '/url', method: 'POST', headers: {}, data: 'messageBody'});
+ $httpBackend.flush();
+ });
+
+
+ it('should set default headers for PUT request', function() {
+ $httpBackend.expect('PUT', '/url', 'messageBody', function(headers) {
+ return headers['Accept'] == 'application/json, text/plain, */*' &&
+ headers['X-Requested-With'] == 'XMLHttpRequest' &&
+ headers['Content-Type'] == 'application/json';
+ }).respond('');
+
+ $http({url: '/url', method: 'PUT', headers: {}, data: 'messageBody'});
+ $httpBackend.flush();
+ });
+
+
+ it('should set default headers for custom HTTP method', function() {
+ $httpBackend.expect('FOO', '/url', undefined, function(headers) {
+ return headers['Accept'] == 'application/json, text/plain, */*' &&
+ headers['X-Requested-With'] == 'XMLHttpRequest';
+ }).respond('');
+
+ $http({url: '/url', method: 'FOO', headers: {}});
+ $httpBackend.flush();
+ });
+
+
+ it('should override default headers with custom', function() {
+ $httpBackend.expect('POST', '/url', 'messageBody', function(headers) {
+ return headers['Accept'] == 'Rewritten' &&
+ headers['X-Requested-With'] == 'XMLHttpRequest' &&
+ headers['Content-Type'] == 'Rewritten';
+ }).respond('');
+
+ $http({url: '/url', method: 'POST', data: 'messageBody', headers: {
+ 'Accept': 'Rewritten',
+ 'Content-Type': 'Rewritten'
+ }});
+ $httpBackend.flush();
+ });
+
+
+ it('should not send Content-Type header if request data/body is undefined', function() {
+ $httpBackend.expect('POST', '/url', undefined, function(headers) {
+ return !headers.hasOwnProperty('Content-Type');
+ }).respond('');
+
+ $http({url: '/url', method: 'POST'});
+ $httpBackend.flush();
+ });
+
+
+ it('should set the XSRF cookie into a XSRF header', inject(function($browser) {
+ function checkXSRF(secret) {
+ return function(headers) {
+ return headers['X-XSRF-TOKEN'] == secret;
+ };
+ }
+
+ $browser.cookies('XSRF-TOKEN', 'secret');
+ $httpBackend.expect('GET', '/url', undefined, checkXSRF('secret')).respond('');
+ $httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond('');
+ $httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond('');
+ $httpBackend.expect('DELETE', '/url', undefined, checkXSRF('secret')).respond('');
+
+ $http({url: '/url', method: 'GET'});
+ $http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}});
+ $http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}});
+ $http({url: '/url', method: 'DELETE', headers: {}});
+
+ $httpBackend.flush();
+ }));
+ });
+
+
+ describe('short methods', function() {
+
+ function checkHeader(name, value) {
+ return function(headers) {
+ return headers[name] == value;
+ };
+ }
+
+ it('should have get()', function() {
+ $httpBackend.expect('GET', '/url').respond('');
+ $http.get('/url');
+ });
+
+
+ it('get() should allow config param', function() {
+ $httpBackend.expect('GET', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
+ $http.get('/url', {headers: {'Custom': 'Header'}});
+ });
+
+
+ it('should have delete()', function() {
+ $httpBackend.expect('DELETE', '/url').respond('');
+ $http['delete']('/url');
+ });
+
+
+ it('delete() should allow config param', function() {
+ $httpBackend.expect('DELETE', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
+ $http['delete']('/url', {headers: {'Custom': 'Header'}});
+ });
+
+
+ it('should have head()', function() {
+ $httpBackend.expect('HEAD', '/url').respond('');
+ $http.head('/url');
+ });
+
+
+ it('head() should allow config param', function() {
+ $httpBackend.expect('HEAD', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
+ $http.head('/url', {headers: {'Custom': 'Header'}});
+ });
+
+
+ it('should have post()', function() {
+ $httpBackend.expect('POST', '/url', 'some-data').respond('');
+ $http.post('/url', 'some-data');
+ });
+
+
+ it('post() should allow config param', function() {
+ $httpBackend.expect('POST', '/url', 'some-data', checkHeader('Custom', 'Header')).respond('');
+ $http.post('/url', 'some-data', {headers: {'Custom': 'Header'}});
+ });
+
+
+ it('should have put()', function() {
+ $httpBackend.expect('PUT', '/url', 'some-data').respond('');
+ $http.put('/url', 'some-data');
+ });
+
+
+ it('put() should allow config param', function() {
+ $httpBackend.expect('PUT', '/url', 'some-data', checkHeader('Custom', 'Header')).respond('');
+ $http.put('/url', 'some-data', {headers: {'Custom': 'Header'}});
+ });
+
+
+ it('should have jsonp()', function() {
+ $httpBackend.expect('JSONP', '/url').respond('');
+ $http.jsonp('/url');
+ });
+
+
+ it('jsonp() should allow config param', function() {
+ $httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
+ $http.jsonp('/url', {headers: {'Custom': 'Header'}});
+ });
+ });
+
+
+ describe('scope.$apply', function() {
+
+ it('should $apply after success callback', function() {
+ $httpBackend.when('GET').respond(200);
+ $http({method: 'GET', url: '/some'});
+ $httpBackend.flush();
+ expect($rootScope.$apply).toHaveBeenCalledOnce();
+ });
+
+
+ it('should $apply after error callback', function() {
+ $httpBackend.when('GET').respond(404);
+ $http({method: 'GET', url: '/some'});
+ $httpBackend.flush();
+ expect($rootScope.$apply).toHaveBeenCalledOnce();
+ });
+
+
+ it('should $apply even if exception thrown during callback', inject(function($exceptionHandler){
+ $httpBackend.when('GET').respond(200);
+ callback.andThrow('error in callback');
+
+ $http({method: 'GET', url: '/some'}).then(callback);
+ $httpBackend.flush();
+ expect($rootScope.$apply).toHaveBeenCalledOnce();
+
+ $exceptionHandler.errors = [];
+ }));
+ });
+
+
+ describe('transformData', function() {
+
+ describe('request', function() {
+
+ describe('default', function() {
+
+ it('should transform object into json', function() {
+ $httpBackend.expect('POST', '/url', '{"one":"two"}').respond('');
+ $http({method: 'POST', url: '/url', data: {one: 'two'}});
+ });
+
+
+ it('should ignore strings', function() {
+ $httpBackend.expect('POST', '/url', 'string-data').respond('');
+ $http({method: 'POST', url: '/url', data: 'string-data'});
+ });
+
+
+ it('should ignore File objects', function() {
+ var file = {
+ some: true,
+ // $httpBackend compares toJson values by default,
+ // we need to be sure it's not serialized into json string
+ test: function(actualValue) {
+ return this === actualValue;
+ }
+ };
+
+ // I'm really sorry for doing this :-D
+ // Unfortunatelly I don't know how to trick toString.apply(obj) comparison
+ spyOn(window, 'isFile').andReturn(true);
+
+ $httpBackend.expect('POST', '/some', file).respond('');
+ $http({method: 'POST', url: '/some', data: file});
+ });
+ });
+
+
+ it('should have access to request headers', function() {
+ $httpBackend.expect('POST', '/url', 'header1').respond(200);
+ $http.post('/url', 'req', {
+ headers: {h1: 'header1'},
+ transformRequest: function(data, headers) {
+ return headers('h1');
+ }
+ }).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should pipeline more functions', function() {
+ function first(d, h) {return d + '-first' + ':' + h('h1')}
+ function second(d) {return uppercase(d)}
+
+ $httpBackend.expect('POST', '/url', 'REQ-FIRST:V1').respond(200);
+ $http.post('/url', 'req', {
+ headers: {h1: 'v1'},
+ transformRequest: [first, second]
+ }).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+ });
+
+
+ describe('response', function() {
+
+ describe('default', function() {
+
+ it('should deserialize json objects', function() {
+ $httpBackend.expect('GET', '/url').respond('{"foo":"bar","baz":23}');
+ $http({method: 'GET', url: '/url'}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toEqual({foo: 'bar', baz: 23});
+ });
+
+
+ it('should deserialize json arrays', function() {
+ $httpBackend.expect('GET', '/url').respond('[1, "abc", {"foo":"bar"}]');
+ $http({method: 'GET', url: '/url'}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toEqual([1, 'abc', {foo: 'bar'}]);
+ });
+
+
+ it('should deserialize json with security prefix', function() {
+ $httpBackend.expect('GET', '/url').respond(')]}\',\n[1, "abc", {"foo":"bar"}]');
+ $http({method: 'GET', url: '/url'}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toEqual([1, 'abc', {foo:'bar'}]);
+ });
+
+
+ it('should deserialize json with security prefix ")]}\'"', function() {
+ $httpBackend.expect('GET', '/url').respond(')]}\'\n\n[1, "abc", {"foo":"bar"}]');
+ $http({method: 'GET', url: '/url'}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toEqual([1, 'abc', {foo:'bar'}]);
+ });
+
+
+ it('should not deserialize tpl beginning with ng expression', function() {
+ $httpBackend.expect('GET', '/url').respond('{{some}}');
+ $http.get('/url').success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toEqual('{{some}}');
+ });
+ });
+
+
+ it('should have access to response headers', function() {
+ $httpBackend.expect('GET', '/url').respond(200, 'response', {h1: 'header1'});
+ $http.get('/url', {
+ transformResponse: function(data, headers) {
+ return headers('h1');
+ }
+ }).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toBe('header1');
+ });
+
+
+ it('should pipeline more functions', function() {
+ function first(d, h) {return d + '-first' + ':' + h('h1')}
+ function second(d) {return uppercase(d)}
+
+ $httpBackend.expect('POST', '/url').respond(200, 'resp', {h1: 'v1'});
+ $http.post('/url', '', {transformResponse: [first, second]}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toBe('RESP-FIRST:V1');
+ });
+ });
+ });
+
+
+ describe('cache', function() {
+
+ var cache;
+
+ beforeEach(inject(function($cacheFactory) {
+ cache = $cacheFactory('testCache');
+ }));
+
+
+ function doFirstCacheRequest(method, respStatus, headers) {
+ $httpBackend.expect(method || 'GET', '/url').respond(respStatus || 200, 'content', headers);
+ $http({method: method || 'GET', url: '/url', cache: cache});
+ $httpBackend.flush();
+ }
+
+
+ it('should cache GET request when cache is provided', inject(function($rootScope) {
+ doFirstCacheRequest();
+
+ $http({method: 'get', url: '/url', cache: cache}).success(callback);
+ $rootScope.$digest();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toBe('content');
+ }));
+
+
+ it('should not cache when cache is not provided', function() {
+ doFirstCacheRequest();
+
+ $httpBackend.expect('GET', '/url').respond();
+ $http({method: 'GET', url: '/url'});
+ });
+
+
+ it('should perform request when cache cleared', function() {
+ doFirstCacheRequest();
+
+ cache.removeAll();
+ $httpBackend.expect('GET', '/url').respond();
+ $http({method: 'GET', url: '/url', cache: cache});
+ });
+
+
+ it('should always call callback asynchronously', function() {
+ doFirstCacheRequest();
+ $http({method: 'get', url: '/url', cache: cache}).then(callback);
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+
+ it('should not cache POST request', function() {
+ doFirstCacheRequest('POST');
+
+ $httpBackend.expect('POST', '/url').respond('content2');
+ $http({method: 'POST', url: '/url', cache: cache}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toBe('content2');
+ });
+
+
+ it('should not cache PUT request', function() {
+ doFirstCacheRequest('PUT');
+
+ $httpBackend.expect('PUT', '/url').respond('content2');
+ $http({method: 'PUT', url: '/url', cache: cache}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toBe('content2');
+ });
+
+
+ it('should not cache DELETE request', function() {
+ doFirstCacheRequest('DELETE');
+
+ $httpBackend.expect('DELETE', '/url').respond(206);
+ $http({method: 'DELETE', url: '/url', cache: cache}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should not cache non 2xx responses', function() {
+ doFirstCacheRequest('GET', 404);
+
+ $httpBackend.expect('GET', '/url').respond('content2');
+ $http({method: 'GET', url: '/url', cache: cache}).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toBe('content2');
+ });
+
+
+ it('should cache the headers as well', inject(function($rootScope) {
+ doFirstCacheRequest('GET', 200, {'content-encoding': 'gzip', 'server': 'Apache'});
+ callback.andCallFake(function(r, s, headers) {
+ expect(headers()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'});
+ expect(headers('server')).toBe('Apache');
+ });
+
+ $http({method: 'GET', url: '/url', cache: cache}).success(callback);
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should not share the cached headers object instance', inject(function($rootScope) {
+ doFirstCacheRequest('GET', 200, {'content-encoding': 'gzip', 'server': 'Apache'});
+ callback.andCallFake(function(r, s, headers) {
+ expect(headers()).toEqual(cache.get('/url')[2]);
+ expect(headers()).not.toBe(cache.get('/url')[2]);
+ });
+
+ $http({method: 'GET', url: '/url', cache: cache}).success(callback);
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should cache status code as well', inject(function($rootScope) {
+ doFirstCacheRequest('GET', 201);
+ callback.andCallFake(function(r, status, h) {
+ expect(status).toBe(201);
+ });
+
+ $http({method: 'get', url: '/url', cache: cache}).success(callback);
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalledOnce();
+ }));
+
+
+ it('should use cache even if second request was made before the first returned', function() {
+ $httpBackend.expect('GET', '/url').respond(201, 'fake-response');
+
+ callback.andCallFake(function(response, status, headers) {
+ expect(response).toBe('fake-response');
+ expect(status).toBe(201);
+ });
+
+ $http({method: 'GET', url: '/url', cache: cache}).success(callback);
+ $http({method: 'GET', url: '/url', cache: cache}).success(callback);
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalled();
+ expect(callback.callCount).toBe(2);
+ });
+
+
+ it('should default to status code 200 and empty headers if cache contains a non-array element',
+ inject(function($rootScope) {
+ cache.put('/myurl', 'simple response');
+ $http.get('/myurl', {cache: cache}).success(function(data, status, headers) {
+ expect(data).toBe('simple response');
+ expect(status).toBe(200);
+ expect(headers()).toEqual({});
+ callback();
+ });
+
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalledOnce();
+ })
+ );
+ });
+
+
+ describe('pendingRequests', function() {
+
+ it('should be an array of pending requests', function() {
+ $httpBackend.when('GET').respond(200);
+ expect($http.pendingRequests.length).toBe(0);
+
+ $http({method: 'get', url: '/some'});
+ expect($http.pendingRequests.length).toBe(1);
+
+ $httpBackend.flush();
+ expect($http.pendingRequests.length).toBe(0);
+ });
+
+
+ it('should update pending requests even when served from cache', inject(function($rootScope) {
+ $httpBackend.when('GET').respond(200);
+
+ $http({method: 'get', url: '/cached', cache: true});
+ $http({method: 'get', url: '/cached', cache: true});
+ expect($http.pendingRequests.length).toBe(2);
+
+ $httpBackend.flush();
+ expect($http.pendingRequests.length).toBe(0);
+
+ $http({method: 'get', url: '/cached', cache: true});
+ expect($http.pendingRequests.length).toBe(1);
+
+ $rootScope.$apply();
+ expect($http.pendingRequests.length).toBe(0);
+ }));
+
+
+ it('should remove the request before firing callbacks', function() {
+ $httpBackend.when('GET').respond(200);
+ $http({method: 'get', url: '/url'}).success(function() {
+ expect($http.pendingRequests.length).toBe(0);
+ });
+
+ expect($http.pendingRequests.length).toBe(1);
+ $httpBackend.flush();
+ });
+ });
+ });
+});
diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js
new file mode 100644
index 00000000..d5f251ea
--- /dev/null
+++ b/test/ng/interpolateSpec.js
@@ -0,0 +1,113 @@
+'use strict';
+
+describe('$interpolate', function() {
+
+ it('should return a function when there are no bindings and textOnly is undefined',
+ inject(function($interpolate) {
+ expect(typeof $interpolate('some text')).toBe('function');
+ }));
+
+
+ it('should return undefined when there are no bindings and textOnly is set to true',
+ inject(function($interpolate) {
+ expect($interpolate('some text', true)).toBeUndefined();
+ }));
+
+ it('should suppress falsy objects', inject(function($interpolate) {
+ expect($interpolate('{{undefined}}')()).toEqual('');
+ expect($interpolate('{{null}}')()).toEqual('');
+ expect($interpolate('{{a.b}}')()).toEqual('');
+ }));
+
+ it('should jsonify objects', inject(function($interpolate) {
+ expect($interpolate('{{ {} }}')()).toEqual('{}');
+ expect($interpolate('{{ true }}')()).toEqual('true');
+ expect($interpolate('{{ false }}')()).toEqual('false');
+ }));
+
+
+ it('should return interpolation function', inject(function($interpolate, $rootScope) {
+ $rootScope.name = 'Misko';
+ expect($interpolate('Hello {{name}}!')($rootScope)).toEqual('Hello Misko!');
+ }));
+
+ describe('provider', function() {
+ beforeEach(module(function($interpolateProvider) {
+ $interpolateProvider.startSymbol('--');
+ $interpolateProvider.endSymbol('--');
+ }));
+
+ it('should not get confused with same markers', inject(function($interpolate) {
+ expect($interpolate('---').parts).toEqual(['---']);
+ expect($interpolate('----')()).toEqual('');
+ expect($interpolate('--1--')()).toEqual('1');
+ }));
+ });
+
+ describe('parseBindings', function() {
+ it('should Parse Text With No Bindings', inject(function($interpolate) {
+ var parts = $interpolate("a").parts;
+ expect(parts.length).toEqual(1);
+ expect(parts[0]).toEqual("a");
+ }));
+
+ it('should Parse Empty Text', inject(function($interpolate) {
+ var parts = $interpolate("").parts;
+ expect(parts.length).toEqual(1);
+ expect(parts[0]).toEqual("");
+ }));
+
+ it('should Parse Inner Binding', inject(function($interpolate) {
+ var parts = $interpolate("a{{b}}C").parts;
+ expect(parts.length).toEqual(3);
+ expect(parts[0]).toEqual("a");
+ expect(parts[1].exp).toEqual("b");
+ expect(parts[1]({b:123})).toEqual(123);
+ expect(parts[2]).toEqual("C");
+ }));
+
+ it('should Parse Ending Binding', inject(function($interpolate) {
+ var parts = $interpolate("a{{b}}").parts;
+ expect(parts.length).toEqual(2);
+ expect(parts[0]).toEqual("a");
+ expect(parts[1].exp).toEqual("b");
+ expect(parts[1]({b:123})).toEqual(123);
+ }));
+
+ it('should Parse Begging Binding', inject(function($interpolate) {
+ var parts = $interpolate("{{b}}c").parts;
+ expect(parts.length).toEqual(2);
+ expect(parts[0].exp).toEqual("b");
+ expect(parts[1]).toEqual("c");
+ }));
+
+ it('should Parse Loan Binding', inject(function($interpolate) {
+ var parts = $interpolate("{{b}}").parts;
+ expect(parts.length).toEqual(1);
+ expect(parts[0].exp).toEqual("b");
+ }));
+
+ it('should Parse Two Bindings', inject(function($interpolate) {
+ var parts = $interpolate("{{b}}{{c}}").parts;
+ expect(parts.length).toEqual(2);
+ expect(parts[0].exp).toEqual("b");
+ expect(parts[1].exp).toEqual("c");
+ }));
+
+ it('should Parse Two Bindings With Text In Middle', inject(function($interpolate) {
+ var parts = $interpolate("{{b}}x{{c}}").parts;
+ expect(parts.length).toEqual(3);
+ expect(parts[0].exp).toEqual("b");
+ expect(parts[1]).toEqual("x");
+ expect(parts[2].exp).toEqual("c");
+ }));
+
+ it('should Parse Multiline', inject(function($interpolate) {
+ var parts = $interpolate('"X\nY{{A\n+B}}C\nD"').parts;
+ expect(parts.length).toEqual(3);
+ expect(parts[0]).toEqual('"X\nY');
+ expect(parts[1].exp).toEqual('A\n+B');
+ expect(parts[2]).toEqual('C\nD"');
+ }));
+ });
+});
diff --git a/test/ng/localeSpec.js b/test/ng/localeSpec.js
new file mode 100644
index 00000000..83c4ecee
--- /dev/null
+++ b/test/ng/localeSpec.js
@@ -0,0 +1,47 @@
+'use strict';
+
+describe('$locale', function() {
+
+ var $locale = new $LocaleProvider().$get();
+
+ it('should have locale id set to en-us', function() {
+ expect($locale.id).toBe('en-us');
+ });
+
+
+ it('should have NUMBER_FORMATS', function() {
+ var numberFormats = $locale.NUMBER_FORMATS;
+ expect(numberFormats).toBeDefined();
+ expect(numberFormats.PATTERNS.length).toBe(2);
+ angular.forEach(numberFormats.PATTERNS, function(pattern) {
+ expect(pattern.minInt).toBeDefined();
+ expect(pattern.minFrac).toBeDefined();
+ expect(pattern.maxFrac).toBeDefined();
+ expect(pattern.posPre).toBeDefined();
+ expect(pattern.posSuf).toBeDefined();
+ expect(pattern.negPre).toBeDefined();
+ expect(pattern.negSuf).toBeDefined();
+ expect(pattern.gSize).toBeDefined();
+ expect(pattern.lgSize).toBeDefined();
+ });
+ });
+
+
+ it('should have DATETIME_FORMATS', function() {
+ var datetime = $locale.DATETIME_FORMATS;
+ expect(datetime).toBeDefined();
+ expect(datetime.DAY.length).toBe(7);
+ expect(datetime.SHORTDAY.length).toBe(7);
+ expect(datetime.SHORTMONTH.length).toBe(12);
+ expect(datetime.MONTH.length).toBe(12);
+ expect(datetime.AMPMS.length).toBe(2);
+ });
+
+
+ it('should return correct plural types', function() {
+ expect($locale.pluralCat(-1)).toBe('other');
+ expect($locale.pluralCat(0)).toBe('other');
+ expect($locale.pluralCat(2)).toBe('other');
+ expect($locale.pluralCat(1)).toBe('one');
+ });
+});
diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js
new file mode 100644
index 00000000..646a9ca0
--- /dev/null
+++ b/test/ng/locationSpec.js
@@ -0,0 +1,883 @@
+'use strict';
+
+/**
+ * Create jasmine.Spy on given method, but ignore calls without arguments
+ * This is helpful when need to spy only setter methods and ignore getters
+ */
+function spyOnlyCallsWithArgs(obj, method) {
+ var spy = spyOn(obj, method);
+ obj[method] = function() {
+ if (arguments.length) return spy.apply(this, arguments);
+ return spy.originalValue.apply(this);
+ };
+ return spy;
+}
+
+
+describe('$location', function() {
+ var url;
+
+ afterEach(function() {
+ // link rewriting used in html5 mode on legacy browsers binds to document.onClick, so we need
+ // to clean this up after each test.
+ jqLite(document).unbind('click');
+ });
+
+ describe('NewUrl', function() {
+ beforeEach(function() {
+ url = new LocationUrl('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
+ });
+
+
+ it('should provide common getters', function() {
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
+ expect(url.protocol()).toBe('http');
+ expect(url.host()).toBe('www.domain.com');
+ expect(url.port()).toBe(9877);
+ expect(url.path()).toBe('/path/b');
+ expect(url.search()).toEqual({search: 'a', b: 'c', d: true});
+ expect(url.hash()).toBe('hash');
+ expect(url.url()).toBe('/path/b?search=a&b=c&d#hash');
+ });
+
+
+ it('path() should change path', function() {
+ url.path('/new/path');
+ expect(url.path()).toBe('/new/path');
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash');
+ });
+
+
+ it('search() should accept string', function() {
+ url.search('x=y&c');
+ expect(url.search()).toEqual({x: 'y', c: true});
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c#hash');
+ });
+
+
+ it('search() should accept object', function() {
+ url.search({one: 1, two: true});
+ expect(url.search()).toEqual({one: 1, two: true});
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
+ });
+
+
+ it('search() should change single parameter', function() {
+ url.search({id: 'old', preserved: true});
+ url.search('id', 'new');
+
+ expect(url.search()).toEqual({id: 'new', preserved: true});
+ });
+
+
+ it('search() should remove single parameter', function() {
+ url.search({id: 'old', preserved: true});
+ url.search('id', null);
+
+ expect(url.search()).toEqual({preserved: true});
+ });
+
+
+ it('hash() should change hash fragment', function() {
+ url.hash('new-hash');
+ expect(url.hash()).toBe('new-hash');
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#new-hash');
+ });
+
+
+ it('url() should change the path, search and hash', function() {
+ url.url('/some/path?a=b&c=d#hhh');
+ expect(url.url()).toBe('/some/path?a=b&c=d#hhh');
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh');
+ expect(url.path()).toBe('/some/path');
+ expect(url.search()).toEqual({a: 'b', c: 'd'});
+ expect(url.hash()).toBe('hhh');
+ });
+
+
+ it('url() should change only hash when no search and path specified', function() {
+ url.url('#some-hash');
+
+ expect(url.hash()).toBe('some-hash');
+ expect(url.url()).toBe('/path/b?search=a&b=c&d#some-hash');
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#some-hash');
+ });
+
+
+ it('url() should change only search and hash when no path specified', function() {
+ url.url('?a=b');
+
+ expect(url.search()).toEqual({a: 'b'});
+ expect(url.hash()).toBe('');
+ expect(url.path()).toBe('/path/b');
+ });
+
+
+ it('url() should reset search and hash when only path specified', function() {
+ url.url('/new/path');
+
+ expect(url.path()).toBe('/new/path');
+ expect(url.search()).toEqual({});
+ expect(url.hash()).toBe('');
+ });
+
+
+ it('replace should set $$replace flag and return itself', function() {
+ expect(url.$$replace).toBe(false);
+
+ url.replace();
+ expect(url.$$replace).toBe(true);
+ expect(url.replace()).toBe(url);
+ });
+
+
+ it('should parse new url', function() {
+ url = new LocationUrl('http://host.com/base');
+ expect(url.path()).toBe('/base');
+
+ url = new LocationUrl('http://host.com/base#');
+ expect(url.path()).toBe('/base');
+ });
+
+
+ it('should prefix path with forward-slash', function() {
+ url = new LocationUrl('http://server/a');
+ url.path('b');
+
+ expect(url.path()).toBe('/b');
+ expect(url.absUrl()).toBe('http://server/b');
+ });
+
+
+ it('should set path to forward-slash when empty', function() {
+ url = new LocationUrl('http://server');
+ expect(url.path()).toBe('/');
+ expect(url.absUrl()).toBe('http://server/');
+ });
+
+
+ it('setters should return Url object to allow chaining', function() {
+ expect(url.path('/any')).toBe(url);
+ expect(url.search('')).toBe(url);
+ expect(url.hash('aaa')).toBe(url);
+ expect(url.url('/some')).toBe(url);
+ });
+
+
+ it('should not preserve old properties when parsing new url', function() {
+ url.$$parse('http://www.domain.com:9877/a');
+
+ expect(url.path()).toBe('/a');
+ expect(url.search()).toEqual({});
+ expect(url.hash()).toBe('');
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/a');
+ });
+
+
+ it('should prepend path with basePath', function() {
+ url = new LocationUrl('http://server/base/abc?a', '/base');
+ expect(url.path()).toBe('/abc');
+ expect(url.search()).toEqual({a: true});
+
+ url.path('/new/path');
+ expect(url.absUrl()).toBe('http://server/base/new/path?a');
+ });
+
+
+ it('should throw error when invalid url given', function() {
+ url = new LocationUrl('http://server.org/base/abc', '/base');
+
+ expect(function() {
+ url.$$parse('http://server.org/path#/path');
+ }).toThrow('Invalid url "http://server.org/path#/path", missing path prefix "/base" !');
+ });
+
+
+ describe('encoding', function() {
+
+ it('should encode special characters', function() {
+ url.path('/a <>#');
+ url.search({'i j': '<>#'});
+ url.hash('<>#');
+
+ expect(url.path()).toBe('/a <>#');
+ expect(url.search()).toEqual({'i j': '<>#'});
+ expect(url.hash()).toBe('<>#');
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
+ });
+
+
+ it('should not encode !$:@', function() {
+ url.path('/!$:@');
+ url.search('');
+ url.hash('!$:@');
+
+ expect(url.absUrl()).toBe('http://www.domain.com:9877/!$:@#!$:@');
+ });
+
+
+ it('should decode special characters', function() {
+ url = new LocationUrl('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
+ expect(url.path()).toBe('/a <>#');
+ expect(url.search()).toEqual({'i j': '<>#'});
+ expect(url.hash()).toBe('x <>#');
+ });
+ });
+ });
+
+
+ describe('HashbangUrl', function() {
+
+ beforeEach(function() {
+ url = new LocationHashbangUrl('http://www.server.org:1234/base#!/path?a=b&c#hash', '!');
+ });
+
+
+ it('should parse hashband url into path and search', function() {
+ expect(url.protocol()).toBe('http');
+ expect(url.host()).toBe('www.server.org');
+ expect(url.port()).toBe(1234);
+ expect(url.path()).toBe('/path');
+ expect(url.search()).toEqual({a: 'b', c: true});
+ expect(url.hash()).toBe('hash');
+ });
+
+
+ it('absUrl() should return hashbang url', function() {
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/path?a=b&c#hash');
+
+ url.path('/new/path');
+ url.search({one: 1});
+ url.hash('hhh');
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/new/path?one=1#hhh');
+ });
+
+
+ it('should preserve query params in base', function() {
+ url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param#/path?a=b&c#hash', '');
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
+
+ url.path('/new/path');
+ url.search({one: 1});
+ url.hash('hhh');
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/new/path?one=1#hhh');
+ });
+
+
+ it('should prefix path with forward-slash', function() {
+ url = new LocationHashbangUrl('http://host.com/base#path', '');
+ expect(url.path()).toBe('/path');
+ expect(url.absUrl()).toBe('http://host.com/base#/path');
+
+ url.path('wrong');
+ expect(url.path()).toBe('/wrong');
+ expect(url.absUrl()).toBe('http://host.com/base#/wrong');
+ });
+
+
+ it('should set path to forward-slash when empty', function() {
+ url = new LocationHashbangUrl('http://server/base#!', '!');
+ url.path('aaa');
+
+ expect(url.path()).toBe('/aaa');
+ expect(url.absUrl()).toBe('http://server/base#!/aaa');
+ });
+
+
+ it('should not preserve old properties when parsing new url', function() {
+ url.$$parse('http://www.server.org:1234/base#!/');
+
+ expect(url.path()).toBe('/');
+ expect(url.search()).toEqual({});
+ expect(url.hash()).toBe('');
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/');
+ });
+
+
+ it('should throw error when invalid url given', function() {
+ expect(function() {
+ url.$$parse('http://server.org/path#/path');
+ }).toThrow('Invalid url "http://server.org/path#/path", missing hash prefix "!" !');
+ });
+
+
+ describe('encoding', function() {
+
+ it('should encode special characters', function() {
+ url.path('/a <>#');
+ url.search({'i j': '<>#'});
+ url.hash('<>#');
+
+ expect(url.path()).toBe('/a <>#');
+ expect(url.search()).toEqual({'i j': '<>#'});
+ expect(url.hash()).toBe('<>#');
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
+ });
+
+
+ it('should not encode !$:@', function() {
+ url.path('/!$:@');
+ url.search('');
+ url.hash('!$:@');
+
+ expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/!$:@#!$:@');
+ });
+
+
+ it('should decode special characters', function() {
+ url = new LocationHashbangUrl('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23', '');
+ expect(url.path()).toBe('/ <>#');
+ expect(url.search()).toEqual({'i j': '<>#'});
+ expect(url.hash()).toBe('x <>#');
+ });
+ });
+ });
+
+
+ function initService(html5Mode, hashPrefix, supportHistory) {
+ return module(function($provide, $locationProvider){
+ $locationProvider.html5Mode(html5Mode);
+ $locationProvider.hashPrefix(hashPrefix);
+ $provide.value('$sniffer', {history: supportHistory});
+ });
+ }
+ function initBrowser(url, basePath) {
+ return function($browser){
+ $browser.url(url);
+ $browser.$$baseHref = basePath;
+ };
+ }
+
+ describe('wiring', function() {
+
+ beforeEach(initService(false, '!', true));
+ beforeEach(inject(initBrowser('http://new.com/a/b#!', '/a/b')));
+
+
+ it('should update $location when browser url changes', inject(function($browser, $location) {
+ spyOn($location, '$$parse').andCallThrough();
+ $browser.url('http://new.com/a/b#!/aaa');
+ $browser.poll();
+ expect($location.absUrl()).toBe('http://new.com/a/b#!/aaa');
+ expect($location.path()).toBe('/aaa');
+ expect($location.$$parse).toHaveBeenCalledOnce();
+ }));
+
+
+ // location.href = '...' fires hashchange event synchronously, so it might happen inside $apply
+ it('should not $apply when browser url changed inside $apply', inject(
+ function($rootScope, $browser, $location) {
+ var OLD_URL = $browser.url(),
+ NEW_URL = 'http://updated.com/url';
+
+
+ $rootScope.$apply(function() {
+ $browser.url(NEW_URL);
+ $browser.poll(); // simulate firing event from browser
+ expect($location.absUrl()).toBe(OLD_URL); // should be async
+ });
+
+ expect($location.absUrl()).toBe(NEW_URL);
+ }));
+
+ // location.href = '...' fires hashchange event synchronously, so it might happen inside $digest
+ it('should not $apply when browser url changed inside $digest', inject(
+ function($rootScope, $browser, $location) {
+ var OLD_URL = $browser.url(),
+ NEW_URL = 'http://updated.com/url',
+ notRunYet = true;
+
+ $rootScope.$watch(function() {
+ if (notRunYet) {
+ notRunYet = false;
+ $browser.url(NEW_URL);
+ $browser.poll(); // simulate firing event from browser
+ expect($location.absUrl()).toBe(OLD_URL); // should be async
+ }
+ });
+
+ $rootScope.$digest();
+ expect($location.absUrl()).toBe(NEW_URL);
+ }));
+
+
+ it('should update browser when $location changes', inject(function($rootScope, $browser, $location) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $location.path('/new/path');
+ expect($browserUrl).not.toHaveBeenCalled();
+ $rootScope.$apply();
+
+ expect($browserUrl).toHaveBeenCalledOnce();
+ expect($browser.url()).toBe('http://new.com/a/b#!/new/path');
+ }));
+
+
+ it('should update browser only once per $apply cycle', inject(function($rootScope, $browser, $location) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $location.path('/new/path');
+
+ $rootScope.$watch(function() {
+ $location.search('a=b');
+ });
+
+ $rootScope.$apply();
+ expect($browserUrl).toHaveBeenCalledOnce();
+ expect($browser.url()).toBe('http://new.com/a/b#!/new/path?a=b');
+ }));
+
+
+ it('should replace browser url when url was replaced at least once',
+ inject(function($rootScope, $location, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $location.path('/n/url').replace();
+ $rootScope.$apply();
+
+ expect($browserUrl).toHaveBeenCalledOnce();
+ expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);
+ }));
+
+
+ it('should update the browser if changed from within a watcher', inject(function($rootScope, $location, $browser) {
+ $rootScope.$watch(function() { return true; }, function() {
+ $location.path('/changed');
+ });
+
+ $rootScope.$digest();
+ expect($browser.url()).toBe('http://new.com/a/b#!/changed');
+ }));
+ });
+
+
+ // html5 history is disabled
+ describe('disabled history', function() {
+
+ it('should use hashbang url with hash prefix', function() {
+ initService(false, '!');
+ inject(
+ initBrowser('http://domain.com/base/index.html#!/a/b', '/base/index.html'),
+ function($rootScope, $location, $browser) {
+ expect($browser.url()).toBe('http://domain.com/base/index.html#!/a/b');
+ $location.path('/new');
+ $location.search({a: true});
+ $rootScope.$apply();
+ expect($browser.url()).toBe('http://domain.com/base/index.html#!/new?a');
+ }
+ );
+ });
+
+
+ it('should use hashbang url without hash prefix', function() {
+ initService(false, '');
+ inject(
+ initBrowser('http://domain.com/base/index.html#/a/b', '/base/index.html'),
+ function($rootScope, $location, $browser) {
+ expect($browser.url()).toBe('http://domain.com/base/index.html#/a/b');
+ $location.path('/new');
+ $location.search({a: true});
+ $rootScope.$apply();
+ expect($browser.url()).toBe('http://domain.com/base/index.html#/new?a');
+ }
+ );
+ });
+ });
+
+
+ // html5 history enabled, but not supported by browser
+ describe('history on old browser', function() {
+
+ afterEach(inject(function($document){
+ dealoc($document);
+ }));
+
+ it('should use hashbang url with hash prefix', function() {
+ initService(true, '!!', false);
+ inject(
+ initBrowser('http://domain.com/base/index.html#!!/a/b', '/base/index.html'),
+ function($rootScope, $location, $browser) {
+ expect($browser.url()).toBe('http://domain.com/base/index.html#!!/a/b');
+ $location.path('/new');
+ $location.search({a: true});
+ $rootScope.$apply();
+ expect($browser.url()).toBe('http://domain.com/base/index.html#!!/new?a');
+ }
+ );
+ });
+
+
+ it('should redirect to hashbang url when new url given', function() {
+ initService(true, '!');
+ inject(
+ initBrowser('http://domain.com/base/new-path/index.html', '/base/index.html'),
+ function($browser, $location) {
+ expect($browser.url()).toBe('http://domain.com/base/index.html#!/new-path/index.html');
+ }
+ );
+ });
+ });
+
+
+ // html5 history enabled and supported by browser
+ describe('history on new browser', function() {
+
+ afterEach(inject(function($document){
+ dealoc($document);
+ }));
+
+ it('should use new url', function() {
+ initService(true, '', true);
+ inject(
+ initBrowser('http://domain.com/base/old/index.html#a', '/base/index.html'),
+ function($rootScope, $location, $browser) {
+ expect($browser.url()).toBe('http://domain.com/base/old/index.html#a');
+ $location.path('/new');
+ $location.search({a: true});
+ $rootScope.$apply();
+ expect($browser.url()).toBe('http://domain.com/base/new?a#a');
+ }
+ );
+ });
+
+
+ it('should rewrite when hashbang url given', function() {
+ initService(true, '!', true);
+ inject(
+ initBrowser('http://domain.com/base/index.html#!/a/b', '/base/index.html'),
+ function($rootScope, $location, $browser) {
+ expect($browser.url()).toBe('http://domain.com/base/a/b');
+ $location.path('/new');
+ $location.hash('abc');
+ $rootScope.$apply();
+ expect($browser.url()).toBe('http://domain.com/base/new#abc');
+ expect($location.path()).toBe('/new');
+ }
+ );
+ });
+
+
+ it('should rewrite when hashbang url given (without hash prefix)', function() {
+ initService(true, '', true);
+ inject(
+ initBrowser('http://domain.com/base/index.html#/a/b', '/base/index.html'),
+ function($rootScope, $location, $browser) {
+ expect($browser.url()).toBe('http://domain.com/base/a/b');
+ expect($location.path()).toBe('/a/b');
+ }
+ );
+ });
+ });
+
+
+ describe('URL_MATCH', function() {
+
+ it('should parse basic url', function() {
+ var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x');
+
+ expect(match[1]).toBe('http');
+ expect(match[3]).toBe('www.angularjs.org');
+ expect(match[6]).toBe('/path');
+ expect(match[8]).toBe('search');
+ expect(match[10]).toBe('hash?x=x');
+ });
+
+
+ it('should parse file://', function() {
+ var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
+
+ expect(match[1]).toBe('file');
+ expect(match[3]).toBe('');
+ expect(match[5]).toBeFalsy();
+ expect(match[6]).toBe('/Users/Shared/misko/work/angular.js/scenario/widgets.html');
+ expect(match[8]).toBeFalsy();
+ });
+
+
+ it('should parse url with "-" in host', function() {
+ var match = URL_MATCH.exec('http://a-b1.c-d.09/path');
+
+ expect(match[1]).toBe('http');
+ expect(match[3]).toBe('a-b1.c-d.09');
+ expect(match[5]).toBeFalsy();
+ expect(match[6]).toBe('/path');
+ expect(match[8]).toBeFalsy();
+ });
+
+
+ it('should parse host without "/" at the end', function() {
+ var match = URL_MATCH.exec('http://host.org');
+ expect(match[3]).toBe('host.org');
+
+ match = URL_MATCH.exec('http://host.org#');
+ expect(match[3]).toBe('host.org');
+
+ match = URL_MATCH.exec('http://host.org?');
+ expect(match[3]).toBe('host.org');
+ });
+
+
+ it('should match with just "/" path', function() {
+ var match = URL_MATCH.exec('http://server/#?book=moby');
+
+ expect(match[10]).toBe('?book=moby');
+ });
+ });
+
+
+ describe('PATH_MATCH', function() {
+
+ it('should parse just path', function() {
+ var match = PATH_MATCH.exec('/path');
+ expect(match[1]).toBe('/path');
+ });
+
+
+ it('should parse path with search', function() {
+ var match = PATH_MATCH.exec('/ppp/a?a=b&c');
+ expect(match[1]).toBe('/ppp/a');
+ expect(match[3]).toBe('a=b&c');
+ });
+
+
+ it('should parse path with hash', function() {
+ var match = PATH_MATCH.exec('/ppp/a#abc?');
+ expect(match[1]).toBe('/ppp/a');
+ expect(match[5]).toBe('abc?');
+ });
+
+
+ it('should parse path with both search and hash', function() {
+ var match = PATH_MATCH.exec('/ppp/a?a=b&c#abc/d?');
+ expect(match[3]).toBe('a=b&c');
+ });
+ });
+
+
+ describe('link rewriting', function() {
+
+ var root, link, originalBrowser, lastEventPreventDefault;
+
+ function configureService(linkHref, html5Mode, supportHist, attrs, content) {
+ module(function($provide, $locationProvider) {
+ var jqRoot = jqLite('<div></div>');
+ attrs = attrs ? ' ' + attrs + ' ' : '';
+ link = jqLite('<a href="' + linkHref + '"' + attrs + '>' + content + '</a>')[0];
+ root = jqRoot.append(link)[0];
+
+ jqLite(document.body).append(jqRoot);
+
+ $provide.value('$document', jqRoot);
+ $provide.value('$sniffer', {history: supportHist});
+ $locationProvider.html5Mode(html5Mode);
+ $locationProvider.hashPrefix('!');
+ });
+ }
+
+ function initBrowser() {
+ return function($browser){
+ $browser.url('http://host.com/base');
+ $browser.$$baseHref = '/base/index.html';
+ };
+ }
+
+ function initLocation() {
+ return function($browser, $location, $document) {
+ originalBrowser = $browser.url();
+ // we have to prevent the default operation, as we need to test absolute links (http://...)
+ // and navigating to these links would kill jstd
+ $document.bind('click', function(e) {
+ lastEventPreventDefault = e.isDefaultPrevented();
+ e.preventDefault();
+ });
+ };
+ }
+
+ function expectRewriteTo($browser, url) {
+ expect(lastEventPreventDefault).toBe(true);
+ expect($browser.url()).toBe(url);
+ }
+
+ function expectNoRewrite($browser) {
+ expect(lastEventPreventDefault).toBe(false);
+ expect($browser.url()).toBe(originalBrowser);
+ }
+
+ afterEach(function() {
+ dealoc(root);
+ dealoc(document.body);
+ });
+
+
+ it('should rewrite rel link to new url when history enabled on new browser', function() {
+ configureService('link?a#b', true, true);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectRewriteTo($browser, 'http://host.com/base/link?a#b');
+ }
+ );
+ });
+
+
+ it('should rewrite abs link to new url when history enabled on new browser', function() {
+ configureService('/base/link?a#b', true, true);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectRewriteTo($browser, 'http://host.com/base/link?a#b');
+ }
+ );
+ });
+
+
+ it('should rewrite rel link to hashbang url when history enabled on old browser', function() {
+ configureService('link?a#b', true, false);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectRewriteTo($browser, 'http://host.com/base/index.html#!/link?a#b');
+ }
+ );
+ });
+
+
+ it('should rewrite abs link to hashbang url when history enabled on old browser', function() {
+ configureService('/base/link?a#b', true, false);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectRewriteTo($browser, 'http://host.com/base/index.html#!/link?a#b');
+ }
+ );
+ });
+
+
+ it('should not rewrite when history disabled', function() {
+ configureService('#new', false);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectNoRewrite($browser);
+ }
+ );
+ });
+
+
+ it('should not rewrite ng-ext-link', function() {
+ configureService('#new', true, true, 'ng-ext-link');
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectNoRewrite($browser);
+ }
+ );
+ });
+
+
+ it('should not rewrite full url links do different domain', function() {
+ configureService('http://www.dot.abc/a?b=c', true);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectNoRewrite($browser);
+ }
+ );
+ });
+
+
+ it('should not rewrite links with target="_blank"', function() {
+ configureService('/a?b=c', true, true, 'target="_blank"');
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectNoRewrite($browser);
+ }
+ );
+ });
+
+
+ it('should not rewrite links with target specified', function() {
+ configureService('/a?b=c', true, true, 'target="some-frame"');
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectNoRewrite($browser);
+ }
+ );
+ });
+
+
+ it('should rewrite full url links to same domain and base path', function() {
+ configureService('http://host.com/base/new', true);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click');
+ expectRewriteTo($browser, 'http://host.com/base/index.html#!/new');
+ }
+ );
+ });
+
+
+ it('should rewrite when clicked span inside link', function() {
+ configureService('some/link', true, true, '', '<span>link</span>');
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ var span = jqLite(link).find('span');
+
+ browserTrigger(span, 'click');
+ expectRewriteTo($browser, 'http://host.com/base/some/link');
+ }
+ );
+ });
+
+
+ // don't run next tests on IE<9, as browserTrigger does not simulate pressed keys
+ if (!(msie < 9)) {
+
+ it('should not rewrite when clicked with ctrl pressed', function() {
+ configureService('/a?b=c', true, true);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click', ['ctrl']);
+ expectNoRewrite($browser);
+ }
+ );
+ });
+
+
+ it('should not rewrite when clicked with meta pressed', function() {
+ configureService('/a?b=c', true, true);
+ inject(
+ initBrowser(),
+ initLocation(),
+ function($browser) {
+ browserTrigger(link, 'click', ['meta']);
+ expectNoRewrite($browser);
+ }
+ );
+ });
+ }
+ });
+});
diff --git a/test/ng/logSpec.js b/test/ng/logSpec.js
new file mode 100644
index 00000000..269057a2
--- /dev/null
+++ b/test/ng/logSpec.js
@@ -0,0 +1,121 @@
+'use strict';
+
+describe('$log', function() {
+ var $window, logger, log, warn, info, error;
+
+
+
+ beforeEach(module(function($provide){
+ $window = {};
+ logger = '';
+ log = function() { logger+= 'log;'; };
+ warn = function() { logger+= 'warn;'; };
+ info = function() { logger+= 'info;'; };
+ error = function() { logger+= 'error;'; };
+
+ $provide.provider('$log', $LogProvider);
+ $provide.value('$exceptionHandler', angular.mock.rethrow);
+ $provide.value('$window', $window);
+ }));
+
+ it('should use console if present', inject(
+ function(){
+ $window.console = {log: log,
+ warn: warn,
+ info: info,
+ error: error};
+ },
+ function($log) {
+ $log.log();
+ $log.warn();
+ $log.info();
+ $log.error();
+ expect(logger).toEqual('log;warn;info;error;');
+ }
+ ));
+
+
+ it('should use console.log() if other not present', inject(
+ function(){
+ $window.console = {log: log};
+ },
+ function($log) {
+ $log.log();
+ $log.warn();
+ $log.info();
+ $log.error();
+ expect(logger).toEqual('log;log;log;log;');
+ }
+ ));
+
+
+ it('should use noop if no console', inject(
+ function($log) {
+ $log.log();
+ $log.warn();
+ $log.info();
+ $log.error();
+ }
+ ));
+
+
+ it("should work in IE where console.error doesn't have apply method", inject(
+ function() {
+ log.apply = log.call =
+ warn.apply = warn.call =
+ info.apply = info.call =
+ error.apply = error.call = null;
+
+ $window.console = {log: log,
+ warn: warn,
+ info: info,
+ error: error};
+ },
+ function($log) {
+ $log.log.apply($log);
+ $log.warn.apply($log);
+ $log.info.apply($log);
+ $log.error.apply($log);
+ expect(logger).toEqual('log;warn;info;error;');
+ })
+ );
+
+
+ describe('$log.error', function() {
+ var e, $log, errorArgs;
+
+ beforeEach(function() {
+ e = new Error('');
+ e.message = undefined;
+ e.sourceURL = undefined;
+ e.line = undefined;
+ e.stack = undefined;
+
+ $log = new $LogProvider().$get[1]({console:{error:function() {
+ errorArgs = [].slice.call(arguments, 0);
+ }}});
+ });
+
+
+ it('should pass error if does not have trace', function() {
+ $log.error('abc', e);
+ expect(errorArgs).toEqual(['abc', e]);
+ });
+
+
+ it('should print stack', function() {
+ e.stack = 'stack';
+ $log.error('abc', e);
+ expect(errorArgs).toEqual(['abc', 'stack']);
+ });
+
+
+ it('should print line', function() {
+ e.message = 'message';
+ e.sourceURL = 'sourceURL';
+ e.line = '123';
+ $log.error('abc', e);
+ expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']);
+ });
+ });
+});
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
new file mode 100644
index 00000000..c98b180c
--- /dev/null
+++ b/test/ng/parseSpec.js
@@ -0,0 +1,631 @@
+'use strict';
+
+describe('parser', function() {
+ describe('lexer', function() {
+ it('should tokenize a string', function() {
+ var tokens = lex("a.bc[22]+1.3|f:'a\\\'c':\"d\\\"e\"");
+ var i = 0;
+ expect(tokens[i].index).toEqual(0);
+ expect(tokens[i].text).toEqual('a.bc');
+
+ i++;
+ expect(tokens[i].index).toEqual(4);
+ expect(tokens[i].text).toEqual('[');
+
+ i++;
+ expect(tokens[i].index).toEqual(5);
+ expect(tokens[i].text).toEqual(22);
+
+ i++;
+ expect(tokens[i].index).toEqual(7);
+ expect(tokens[i].text).toEqual(']');
+
+ i++;
+ expect(tokens[i].index).toEqual(8);
+ expect(tokens[i].text).toEqual('+');
+
+ i++;
+ expect(tokens[i].index).toEqual(9);
+ expect(tokens[i].text).toEqual(1.3);
+
+ i++;
+ expect(tokens[i].index).toEqual(12);
+ expect(tokens[i].text).toEqual('|');
+
+ i++;
+ expect(tokens[i].index).toEqual(13);
+ expect(tokens[i].text).toEqual('f');
+
+ i++;
+ expect(tokens[i].index).toEqual(14);
+ expect(tokens[i].text).toEqual(':');
+
+ i++;
+ expect(tokens[i].index).toEqual(15);
+ expect(tokens[i].string).toEqual("a'c");
+
+ i++;
+ expect(tokens[i].index).toEqual(21);
+ expect(tokens[i].text).toEqual(':');
+
+ i++;
+ expect(tokens[i].index).toEqual(22);
+ expect(tokens[i].string).toEqual('d"e');
+ });
+
+ it('should tokenize undefined', function() {
+ var tokens = lex("undefined");
+ var i = 0;
+ expect(tokens[i].index).toEqual(0);
+ expect(tokens[i].text).toEqual('undefined');
+ expect(undefined).toEqual(tokens[i].fn());
+ });
+
+ it('should tokenize quoted string', function() {
+ var str = "['\\'', \"\\\"\"]";
+ var tokens = lex(str);
+
+ expect(tokens[1].index).toEqual(1);
+ expect(tokens[1].string).toEqual("'");
+
+ expect(tokens[3].index).toEqual(7);
+ expect(tokens[3].string).toEqual('"');
+ });
+
+ it('should tokenize escaped quoted string', function() {
+ var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
+ var tokens = lex(str);
+
+ expect(tokens[0].string).toEqual('"\n\f\r\t\v\u00A0');
+ });
+
+ it('should tokenize unicode', function() {
+ var tokens = lex('"\\u00A0"');
+ expect(tokens.length).toEqual(1);
+ expect(tokens[0].string).toEqual('\u00a0');
+ });
+
+ it('should ignore whitespace', function() {
+ var tokens = lex("a \t \n \r b");
+ expect(tokens[0].text).toEqual('a');
+ expect(tokens[1].text).toEqual('b');
+ });
+
+ it('should tokenize relation', function() {
+ var tokens = lex("! == != < > <= >=");
+ expect(tokens[0].text).toEqual('!');
+ expect(tokens[1].text).toEqual('==');
+ expect(tokens[2].text).toEqual('!=');
+ expect(tokens[3].text).toEqual('<');
+ expect(tokens[4].text).toEqual('>');
+ expect(tokens[5].text).toEqual('<=');
+ expect(tokens[6].text).toEqual('>=');
+ });
+
+ it('should tokenize statements', function() {
+ var tokens = lex("a;b;");
+ expect(tokens[0].text).toEqual('a');
+ expect(tokens[1].text).toEqual(';');
+ expect(tokens[2].text).toEqual('b');
+ expect(tokens[3].text).toEqual(';');
+ });
+
+ it('should tokenize function invocation', function() {
+ var tokens = lex("a()")
+ expect(map(tokens, function(t) { return t.text;})).toEqual(['a', '(', ')']);
+ });
+
+ it('should tokenize method invocation', function() {
+ var tokens = lex("a.b.c (d) - e.f()");
+ expect(map(tokens, function(t) { return t.text;})).
+ toEqual(['a.b', '.', 'c', '(', 'd', ')', '-', 'e', '.', 'f', '(', ')']);
+ });
+
+ it('should tokenize number', function() {
+ var tokens = lex("0.5");
+ expect(tokens[0].text).toEqual(0.5);
+ });
+
+ it('should tokenize negative number', inject(function($rootScope) {
+ var value = $rootScope.$eval("-0.5");
+ expect(value).toEqual(-0.5);
+
+ value = $rootScope.$eval("{a:-0.5}");
+ expect(value).toEqual({a:-0.5});
+ }));
+
+ it('should tokenize number with exponent', inject(function($rootScope) {
+ var tokens = lex("0.5E-10");
+ expect(tokens[0].text).toEqual(0.5E-10);
+ expect($rootScope.$eval("0.5E-10")).toEqual(0.5E-10);
+
+ tokens = lex("0.5E+10");
+ expect(tokens[0].text).toEqual(0.5E+10);
+ }));
+
+ it('should throws exception for invalid exponent', function() {
+ expect(function() {
+ lex("0.5E-");
+ }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-].'));
+
+ expect(function() {
+ lex("0.5E-A");
+ }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].'));
+ });
+
+ it('should tokenize number starting with a dot', function() {
+ var tokens = lex(".5");
+ expect(tokens[0].text).toEqual(0.5);
+ });
+
+ it('should throw error on invalid unicode', function() {
+ expect(function() {
+ lex("'\\u1''bla'");
+ }).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']."));
+ });
+ });
+
+ var scope, $filterProvider;
+ beforeEach(module(['$filterProvider', function (filterProvider) {
+ $filterProvider = filterProvider;
+ }]));
+ beforeEach(inject(function ($rootScope) {
+ scope = $rootScope;
+ }));
+
+ it('should parse expressions', function() {
+ expect(scope.$eval("-1")).toEqual(-1);
+ expect(scope.$eval("1 + 2.5")).toEqual(3.5);
+ expect(scope.$eval("1 + -2.5")).toEqual(-1.5);
+ expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4);
+ expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5);
+ expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4);
+ expect(scope.$eval("1/2*3")).toEqual(1/2*3);
+ });
+
+ it('should parse comparison', function() {
+ expect(scope.$eval("false")).toBeFalsy();
+ expect(scope.$eval("!true")).toBeFalsy();
+ expect(scope.$eval("1==1")).toBeTruthy();
+ expect(scope.$eval("1!=2")).toBeTruthy();
+ expect(scope.$eval("1<2")).toBeTruthy();
+ expect(scope.$eval("1<=1")).toBeTruthy();
+ expect(scope.$eval("1>2")).toEqual(1>2);
+ expect(scope.$eval("2>=1")).toEqual(2>=1);
+ expect(scope.$eval("true==2<3")).toEqual(true === 2<3);
+ });
+
+ it('should parse logical', function() {
+ expect(scope.$eval("0&&2")).toEqual(0&&2);
+ expect(scope.$eval("0||2")).toEqual(0||2);
+ expect(scope.$eval("0||1&&2")).toEqual(0||1&&2);
+ });
+
+ it('should parse string', function() {
+ expect(scope.$eval("'a' + 'b c'")).toEqual("ab c");
+ });
+
+ it('should parse filters', function() {
+ $filterProvider.register('substring', valueFn(function(input, start, end) {
+ return input.substring(start, end);
+ }));
+
+ expect(function() {
+ scope.$eval("1|nonexistent");
+ }).toThrow(new Error("Unknown provider: nonexistentFilterProvider <- nonexistentFilter"));
+
+ scope.offset = 3;
+ expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc");
+ expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC");
+ });
+
+ it('should access scope', function() {
+ scope.a = 123;
+ scope.b = {c: 456};
+ expect(scope.$eval("a", scope)).toEqual(123);
+ expect(scope.$eval("b.c", scope)).toEqual(456);
+ expect(scope.$eval("x.y.z", scope)).not.toBeDefined();
+ });
+
+ it('should support property names that colide with native object properties', function() {
+ // regression
+ scope.watch = 1;
+ scope.constructor = 2;
+ scope.toString = 3;
+
+ expect(scope.$eval('watch', scope)).toBe(1);
+ expect(scope.$eval('constructor', scope)).toBe(2);
+ expect(scope.$eval('toString', scope)).toBe(3);
+ });
+
+ it('should evaluate grouped expressions', function() {
+ expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3);
+ });
+
+ it('should evaluate assignments', function() {
+ expect(scope.$eval("a=12")).toEqual(12);
+ expect(scope.a).toEqual(12);
+
+ expect(scope.$eval("x.y.z=123;")).toEqual(123);
+ expect(scope.x.y.z).toEqual(123);
+
+ expect(scope.$eval("a=123; b=234")).toEqual(234);
+ expect(scope.a).toEqual(123);
+ expect(scope.b).toEqual(234);
+ });
+
+ it('should evaluate function call without arguments', function() {
+ scope['const'] = function(a,b){return 123;};
+ expect(scope.$eval("const()")).toEqual(123);
+ });
+
+ it('should evaluate function call with arguments', function() {
+ scope.add = function(a,b) {
+ return a+b;
+ };
+ expect(scope.$eval("add(1,2)")).toEqual(3);
+ });
+
+ it('should evaluate function call from a return value', function() {
+ scope.val = 33;
+ scope.getter = function() { return function() { return this.val; }};
+ expect(scope.$eval("getter()()")).toBe(33);
+ });
+
+ it('should evaluate multiplication and division', function() {
+ scope.taxRate = 8;
+ scope.subTotal = 100;
+ expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8);
+ expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8);
+ });
+
+ it('should evaluate array', function() {
+ expect(scope.$eval("[]").length).toEqual(0);
+ expect(scope.$eval("[1, 2]").length).toEqual(2);
+ expect(scope.$eval("[1, 2]")[0]).toEqual(1);
+ expect(scope.$eval("[1, 2]")[1]).toEqual(2);
+ });
+
+ it('should evaluate array access', function() {
+ expect(scope.$eval("[1][0]")).toEqual(1);
+ expect(scope.$eval("[[1]][0][0]")).toEqual(1);
+ expect(scope.$eval("[].length")).toEqual(0);
+ expect(scope.$eval("[1, 2].length")).toEqual(2);
+ });
+
+ it('should evaluate object', function() {
+ expect(toJson(scope.$eval("{}"))).toEqual("{}");
+ expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}');
+ expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}');
+ expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}');
+ });
+
+ it('should evaluate object access', function() {
+ expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC");
+ });
+
+ it('should evaluate JSON', function() {
+ expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]");
+ expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]');
+ });
+
+ it('should evaluate multiple statements', function() {
+ expect(scope.$eval("a=1;b=3;a+b")).toEqual(4);
+ expect(scope.$eval(";;1;;")).toEqual(1);
+ });
+
+ it('should evaluate object methods in correct context (this)', function() {
+ var C = function () {
+ this.a = 123;
+ };
+ C.prototype.getA = function() {
+ return this.a;
+ };
+
+ scope.obj = new C();
+ expect(scope.$eval("obj.getA()")).toEqual(123);
+ expect(scope.$eval("obj['getA']()")).toEqual(123);
+ });
+
+ it('should evaluate methods in correct context (this) in argument', function() {
+ var C = function () {
+ this.a = 123;
+ };
+ C.prototype.sum = function(value) {
+ return this.a + value;
+ };
+ C.prototype.getA = function() {
+ return this.a;
+ };
+
+ scope.obj = new C();
+ expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246);
+ expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246);
+ });
+
+ it('should evaluate objects on scope context', function() {
+ scope.a = "abc";
+ expect(scope.$eval("{a:a}").a).toEqual("abc");
+ });
+
+ it('should evaluate field access on function call result', function() {
+ scope.a = function() {
+ return {name:'misko'};
+ };
+ expect(scope.$eval("a().name")).toEqual("misko");
+ });
+
+ it('should evaluate field access after array access', function () {
+ scope.items = [{}, {name:'misko'}];
+ expect(scope.$eval('items[1].name')).toEqual("misko");
+ });
+
+ it('should evaluate array assignment', function() {
+ scope.items = [];
+
+ expect(scope.$eval('items[1] = "abc"')).toEqual("abc");
+ expect(scope.$eval('items[1]')).toEqual("abc");
+// Dont know how to make this work....
+// expect(scope.$eval('books[1] = "moby"')).toEqual("moby");
+// expect(scope.$eval('books[1]')).toEqual("moby");
+ });
+
+ it('should evaluate grouped filters', function() {
+ scope.name = 'MISKO';
+ expect(scope.$eval('n = (name|lowercase)')).toEqual('misko');
+ expect(scope.$eval('n')).toEqual('misko');
+ });
+
+ it('should evaluate remainder', function() {
+ expect(scope.$eval('1%2')).toEqual(1);
+ });
+
+ it('should evaluate sum with undefined', function() {
+ expect(scope.$eval('1+undefined')).toEqual(1);
+ expect(scope.$eval('undefined+1')).toEqual(1);
+ });
+
+ it('should throw exception on non-closed bracket', function() {
+ expect(function() {
+ scope.$eval('[].count(');
+ }).toThrow('Unexpected end of expression: [].count(');
+ });
+
+ it('should evaluate double negation', function() {
+ expect(scope.$eval('true')).toBeTruthy();
+ expect(scope.$eval('!true')).toBeFalsy();
+ expect(scope.$eval('!!true')).toBeTruthy();
+ expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a');
+ });
+
+ it('should evaluate negation', function() {
+ expect(scope.$eval("!false || true")).toEqual(!false || true);
+ expect(scope.$eval("!11 == 10")).toEqual(!11 == 10);
+ expect(scope.$eval("12/6/2")).toEqual(12/6/2);
+ });
+
+ it('should evaluate exclamation mark', function() {
+ expect(scope.$eval('suffix = "!"')).toEqual('!');
+ });
+
+ it('should evaluate minus', function() {
+ expect(scope.$eval("{a:'-'}")).toEqual({a: "-"});
+ });
+
+ it('should evaluate undefined', function() {
+ expect(scope.$eval("undefined")).not.toBeDefined();
+ expect(scope.$eval("a=undefined")).not.toBeDefined();
+ expect(scope.a).not.toBeDefined();
+ });
+
+ it('should allow assignment after array dereference', function() {
+ scope.obj = [{}];
+ scope.$eval('obj[0].name=1');
+ expect(scope.obj.name).toBeUndefined();
+ expect(scope.obj[0].name).toEqual(1);
+ });
+
+ it('should short-circuit AND operator', function() {
+ scope.run = function() {
+ throw "IT SHOULD NOT HAVE RUN";
+ };
+ expect(scope.$eval('false && run()')).toBe(false);
+ });
+
+ it('should short-circuit OR operator', function() {
+ scope.run = function() {
+ throw "IT SHOULD NOT HAVE RUN";
+ };
+ expect(scope.$eval('true || run()')).toBe(true);
+ });
+
+
+ describe('promises', function() {
+ var deferred, promise, q;
+
+ beforeEach(inject(function($q) {
+ q = $q;
+ deferred = q.defer();
+ promise = deferred.promise;
+ }));
+
+ describe('{{promise}}', function() {
+ it('should evaluated resolved promise and get its value', function() {
+ deferred.resolve('hello!');
+ scope.greeting = promise;
+ expect(scope.$eval('greeting')).toBe(undefined);
+ scope.$digest();
+ expect(scope.$eval('greeting')).toBe('hello!');
+ });
+
+
+ it('should evaluated rejected promise and ignore the rejection reason', function() {
+ deferred.reject('sorry');
+ scope.greeting = promise;
+ expect(scope.$eval('gretting')).toBe(undefined);
+ scope.$digest();
+ expect(scope.$eval('greeting')).toBe(undefined);
+ });
+
+
+ it('should evaluate a promise and eventualy get its value', function() {
+ scope.greeting = promise;
+ expect(scope.$eval('greeting')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('greeting')).toBe(undefined);
+
+ deferred.resolve('hello!');
+ expect(scope.$eval('greeting')).toBe(undefined);
+ scope.$digest();
+ expect(scope.$eval('greeting')).toBe('hello!');
+ });
+
+
+ it('should evaluate a promise and eventualy ignore its rejection', function() {
+ scope.greeting = promise;
+ expect(scope.$eval('greeting')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('greeting')).toBe(undefined);
+
+ deferred.reject('sorry');
+ expect(scope.$eval('greeting')).toBe(undefined);
+ scope.$digest();
+ expect(scope.$eval('greeting')).toBe(undefined);
+ });
+ });
+
+ describe('dereferencing', function() {
+ it('should evaluate and dereference properties leading to and from a promise', function() {
+ scope.obj = {greeting: promise};
+ expect(scope.$eval('obj.greeting')).toBe(undefined);
+ expect(scope.$eval('obj.greeting.polite')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('obj.greeting')).toBe(undefined);
+ expect(scope.$eval('obj.greeting.polite')).toBe(undefined);
+
+ deferred.resolve({polite: 'Good morning!'});
+ scope.$digest();
+ expect(scope.$eval('obj.greeting')).toEqual({polite: 'Good morning!'});
+ expect(scope.$eval('obj.greeting.polite')).toBe('Good morning!');
+ });
+
+ it('should evaluate and dereference properties leading to and from a promise via bracket ' +
+ 'notation', function() {
+ scope.obj = {greeting: promise};
+ expect(scope.$eval('obj["greeting"]')).toBe(undefined);
+ expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('obj["greeting"]')).toBe(undefined);
+ expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined);
+
+ deferred.resolve({polite: 'Good morning!'});
+ scope.$digest();
+ expect(scope.$eval('obj["greeting"]')).toEqual({polite: 'Good morning!'});
+ expect(scope.$eval('obj["greeting"]["polite"]')).toBe('Good morning!');
+ });
+
+
+ it('should evaluate and dereference array references leading to and from a promise',
+ function() {
+ scope.greetings = [promise];
+ expect(scope.$eval('greetings[0]')).toBe(undefined);
+ expect(scope.$eval('greetings[0][0]')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('greetings[0]')).toBe(undefined);
+ expect(scope.$eval('greetings[0][0]')).toBe(undefined);
+
+ deferred.resolve(['Hi!', 'Cau!']);
+ scope.$digest();
+ expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']);
+ expect(scope.$eval('greetings[0][0]')).toBe('Hi!');
+ });
+
+
+ it('should evaluate and dereference promises used as function arguments', function() {
+ scope.greet = function(name) { return 'Hi ' + name + '!'; };
+ scope.name = promise;
+ expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
+
+ scope.$digest();
+ expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
+
+ deferred.resolve('Veronica');
+ expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
+
+ scope.$digest();
+ expect(scope.$eval('greet(name)')).toBe('Hi Veronica!');
+ });
+
+
+ it('should evaluate and dereference promises used as array indexes', function() {
+ scope.childIndex = promise;
+ scope.kids = ['Adam', 'Veronica', 'Elisa'];
+ expect(scope.$eval('kids[childIndex]')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('kids[childIndex]')).toBe(undefined);
+
+ deferred.resolve(1);
+ expect(scope.$eval('kids[childIndex]')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('kids[childIndex]')).toBe('Veronica');
+ });
+
+
+ it('should evaluate and dereference promises used as keys in bracket notation', function() {
+ scope.childKey = promise;
+ scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'};
+
+ expect(scope.$eval('kids[childKey]')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('kids[childKey]')).toBe(undefined);
+
+ deferred.resolve('v');
+ expect(scope.$eval('kids[childKey]')).toBe(undefined);
+
+ scope.$digest();
+ expect(scope.$eval('kids[childKey]')).toBe('Veronica');
+ });
+
+
+ it('should not mess with the promise if it was not directly evaluated', function() {
+ scope.obj = {greeting: promise, username: 'hi'};
+ var obj = scope.$eval('obj');
+ expect(obj.username).toEqual('hi');
+ expect(typeof obj.greeting.then).toBe('function');
+ });
+ });
+ });
+
+
+ describe('assignable', function() {
+ it('should expose assignment function', inject(function($parse) {
+ var fn = $parse('a');
+ expect(fn.assign).toBeTruthy();
+ var scope = {};
+ fn.assign(scope, 123);
+ expect(scope).toEqual({a:123});
+ }));
+ });
+
+
+ describe('locals', function() {
+ it('should expose local variables', inject(function($parse) {
+ expect($parse('a')({a: 0}, {a: 1})).toEqual(1);
+ expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3);
+ }));
+
+ it('should expose traverse locals', inject(function($parse) {
+ expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1);
+ expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1);
+ expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined);
+ }));
+ });
+});
diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js
new file mode 100644
index 00000000..a230d1de
--- /dev/null
+++ b/test/ng/qSpec.js
@@ -0,0 +1,831 @@
+'use strict';
+
+/**
+ http://wiki.commonjs.org/wiki/Promises
+ http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript
+
+ Q: https://github.com/kriskowal/q
+ https://github.com/kriskowal/q/blob/master/design/README.js
+ https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md
+ http://jsconf.eu/2010/speaker/commonjs_i_promise_by_kris_kow.html
+ - good walkthrough of the Q api's and design, jump to 15:30
+
+ twisted: http://twistedmatrix.com/documents/11.0.0/api/twisted.internet.defer.Deferred.html
+ dojo: https://github.com/dojo/dojo/blob/master/_base/Deferred.js
+ http://dojotoolkit.org/api/1.6/dojo/Deferred
+ http://dojotoolkit.org/documentation/tutorials/1.6/promises/
+ when.js: https://github.com/briancavalier/when.js
+ DART: http://www.dartlang.org/docs/api/Promise.html#Promise::Promise
+ http://code.google.com/p/dart/source/browse/trunk/dart/corelib/src/promise.dart
+ http://codereview.chromium.org/8271014/patch/11003/12005
+ https://chromereviews.googleplex.com/3365018/
+ WinJS: http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx
+
+ http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/Future.html
+ http://en.wikipedia.org/wiki/Futures_and_promises
+ http://wiki.ecmascript.org/doku.php?id=strawman:deferred_functions
+ http://wiki.ecmascript.org/doku.php?id=strawman:async_functions
+
+
+ http://jsperf.com/throw-vs-return
+*/
+
+describe('q', function() {
+ var q, defer, deferred, promise, log;
+
+ /**
+ * Creates a callback that logs its invocation in `log`.
+ *
+ * @param {(number|string)} name Suffix for 'success' name. e.g. success(1) => success1
+ * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
+ * value is returned.
+ * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
+ */
+ function success(name, returnVal, throwReturnVal) {
+ var returnValDefined = (arguments.length >= 2);
+
+ return function() {
+ name = 'success' + (name || '');
+ var args = toJson(sliceArgs(arguments)).replace(/(^\[|"|\]$)/g, '');
+ log.push(name + '(' + args + ')');
+ returnVal = returnValDefined ? returnVal : arguments[0];
+ if (throwReturnVal) throw returnVal;
+ return returnVal;
+ }
+ }
+
+
+ /**
+ * Creates a callback that logs its invocation in `log`.
+ *
+ * @param {(number|string)} name Suffix for 'error' name. e.g. error(1) => error1
+ * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
+ * value is rethrown.
+ * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
+ */
+ function error(name, returnVal, throwReturnVal) {
+ var returnValDefined = (arguments.length >= 2);
+
+ return function(){
+ name = 'error' + (name || '');
+ log.push(name + '(' + [].join.call(arguments, ',') + ')');
+ returnVal = returnValDefined ? returnVal : q.reject(arguments[0]);
+ if (throwReturnVal) throw returnVal;
+ return returnVal;
+ }
+ }
+
+
+ /** helper for synchronous resolution of deferred */
+ function syncResolve(deferred, result) {
+ deferred.resolve(result);
+ mockNextTick.flush();
+ }
+
+
+ /** helper for synchronous rejection of deferred */
+ function syncReject(deferred, reason) {
+ deferred.reject(reason);
+ mockNextTick.flush();
+ }
+
+
+ /** converts the `log` to a '; '-separated string */
+ function logStr() {
+ return log.join('; ');
+ }
+
+
+ var mockNextTick = {
+ nextTick: function(task) {
+ mockNextTick.queue.push(task);
+ },
+ queue: [],
+ flush: function() {
+ if (!mockNextTick.queue.length) throw new Error('Nothing to be flushed!');
+ while (mockNextTick.queue.length) {
+ var queue = mockNextTick.queue;
+ mockNextTick.queue = [];
+ forEach(queue, function(task) {
+ try {
+ task();
+ } catch(e) {
+ dump('exception in mockNextTick:', e, e.name, e.message, task);
+ }
+ });
+ }
+ }
+ }
+
+
+ beforeEach(function() {
+ q = qFactory(mockNextTick.nextTick, noop),
+ defer = q.defer;
+ deferred = defer()
+ promise = deferred.promise;
+ log = [];
+ mockNextTick.queue = [];
+ });
+
+
+ afterEach(function() {
+ expect(mockNextTick.queue.length).toBe(0);
+ });
+
+
+ describe('defer', function() {
+ it('should create a new deferred', function() {
+ expect(deferred.promise).toBeDefined();
+ expect(deferred.resolve).toBeDefined();
+ expect(deferred.reject).toBeDefined();
+ });
+
+
+ describe('resolve', function() {
+ it('should fulfill the promise and execute all success callbacks in the registration order',
+ function() {
+ promise.then(success(1), error());
+ promise.then(success(2), error());
+ expect(logStr()).toBe('');
+
+ deferred.resolve('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(foo); success2(foo)');
+ });
+
+
+ it('should do nothing if a promise was previously resolved', function() {
+ promise.then(success(), error());
+ expect(logStr()).toBe('');
+
+ deferred.resolve('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success(foo)');
+
+ log = [];
+ deferred.resolve('bar');
+ deferred.reject('baz');
+ expect(mockNextTick.queue.length).toBe(0);
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should do nothing if a promise was previously rejected', function() {
+ promise.then(success(), error());
+ expect(logStr()).toBe('');
+
+ deferred.reject('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error(foo)');
+
+ log = [];
+ deferred.resolve('bar');
+ deferred.reject('baz');
+ expect(mockNextTick.queue.length).toBe(0);
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should allow deferred resolution with a new promise', function() {
+ var deferred2 = defer();
+ promise.then(success(), error());
+
+ deferred.resolve(deferred2.promise);
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+
+ deferred2.resolve('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success(foo)');
+ });
+
+
+ it('should call the callback in the next turn', function() {
+ promise.then(success());
+ expect(logStr()).toBe('');
+
+ deferred.resolve('foo');
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(logStr()).toBe('success(foo)');
+ });
+
+
+ it('should support non-bound execution', function() {
+ var resolver = deferred.resolve;
+ promise.then(success(), error());
+ resolver('detached');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success(detached)');
+ });
+
+
+ it('should not break if a callbacks registers another callback', function() {
+ promise.then(function() {
+ log.push('outer');
+ promise.then(function() {
+ log.push('inner');
+ });
+ });
+
+ deferred.resolve('foo');
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(logStr()).toBe('outer; inner');
+ });
+
+
+ it('should not break if a callbacks tries to resolve the deferred again', function() {
+ promise.then(function(val) {
+ log.push('success1(' + val + ')');
+ deferred.resolve('bar');
+ });
+
+ promise.then(success(2));
+
+ deferred.resolve('foo');
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(foo); success2(foo)');
+ });
+ });
+
+
+ describe('reject', function() {
+ it('should reject the promise and execute all error callbacks in the registration order',
+ function() {
+ promise.then(success(), error(1));
+ promise.then(success(), error(2));
+ expect(logStr()).toBe('');
+
+ deferred.reject('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(foo); error2(foo)');
+ });
+
+
+ it('should do nothing if a promise was previously resolved', function() {
+ promise.then(success(1), error(1));
+ expect(logStr()).toBe('');
+
+ deferred.resolve('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(foo)');
+
+ log = [];
+ deferred.reject('bar');
+ deferred.resolve('baz');
+ expect(mockNextTick.queue.length).toBe(0);
+ expect(logStr()).toBe('');
+
+ promise.then(success(2), error(2))
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success2(foo)');
+ });
+
+
+ it('should do nothing if a promise was previously rejected', function() {
+ promise.then(success(1), error(1));
+ expect(logStr()).toBe('');
+
+ deferred.reject('foo');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(foo)');
+
+ log = [];
+ deferred.reject('bar');
+ deferred.resolve('baz');
+ expect(mockNextTick.queue.length).toBe(0);
+ expect(logStr()).toBe('');
+
+ promise.then(success(2), error(2))
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error2(foo)');
+ });
+
+
+ it('should not defer rejection with a new promise', function() {
+ var deferred2 = defer();
+ promise.then(success(), error());
+
+ deferred.reject(deferred2.promise);
+ mockNextTick.flush();
+ expect(logStr()).toBe('error([object Object])');
+ });
+
+
+ it('should call the error callback in the next turn', function() {
+ promise.then(success(), error());
+ expect(logStr()).toBe('');
+
+ deferred.reject('foo');
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(logStr()).toBe('error(foo)');
+ });
+
+
+ it('should support non-bound execution', function() {
+ var rejector = deferred.reject;
+ promise.then(success(), error());
+ rejector('detached');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error(detached)');
+ });
+ });
+
+
+ describe('promise', function() {
+ it('should have a then method', function() {
+ expect(typeof promise.then).toBe('function');
+ });
+
+
+ describe('then', function() {
+ it('should allow registration of a success callback without an errback and resolve',
+ function() {
+ promise.then(success());
+ syncResolve(deferred, 'foo');
+ expect(logStr()).toBe('success(foo)');
+ });
+
+ it('should allow registration of a success callback without an errback and reject',
+ function() {
+ promise.then(success());
+ syncReject(deferred, 'foo');
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should allow registration of an errback without a success callback and reject',
+ function() {
+ promise.then(null, error());
+ syncReject(deferred, 'oops!');
+ expect(logStr()).toBe('error(oops!)');
+ });
+
+
+ it('should allow registration of an errback without a success callback and resolve',
+ function() {
+ promise.then(null, error());
+ syncResolve(deferred, 'done');
+ expect(logStr()).toBe('');
+ });
+
+
+ it('should resolve all callbacks with the original value', function() {
+ promise.then(success('A', 'aVal'), error());
+ promise.then(success('B', 'bErr', true), error());
+ promise.then(success('C', q.reject('cReason')), error());
+ promise.then(success('D', 'dVal'), error());
+
+ expect(logStr()).toBe('');
+ syncResolve(deferred, 'yup');
+ expect(logStr()).toBe('successA(yup); successB(yup); successC(yup); successD(yup)');
+ });
+
+
+ it('should reject all callbacks with the original reason', function() {
+ promise.then(success(), error('A', 'aVal'));
+ promise.then(success(), error('B', 'bEr', true));
+ promise.then(success(), error('C', q.reject('cReason')));
+ promise.then(success(), error('D', 'dVal'));
+
+ expect(logStr()).toBe('');
+ syncReject(deferred, 'noo!');
+ expect(logStr()).toBe('errorA(noo!); errorB(noo!); errorC(noo!); errorD(noo!)');
+ });
+
+
+ it('should propagate resolution and rejection between dependent promises', function() {
+ promise.then(success(1, 'x'), error('1')).
+ then(success(2, 'y', true), error('2')).
+ then(success(3), error(3, 'z', true)).
+ then(success(4), error(4, 'done')).
+ then(success(5), error(5));
+
+ expect(logStr()).toBe('');
+ syncResolve(deferred, 'sweet!');
+ expect(log).toEqual(['success1(sweet!)',
+ 'success2(x)',
+ 'error3(y)',
+ 'error4(z)',
+ 'success5(done)']);
+ });
+
+
+ it('should reject a derived promise if an exception is thrown while resolving its parent',
+ function() {
+ promise.then(success(1, 'oops', true)).
+ then(success(2), error(2));
+ syncResolve(deferred, 'done!');
+ expect(logStr()).toBe('success1(done!); error2(oops)');
+ });
+
+
+ it('should reject a derived promise if an exception is thrown while rejecting its parent',
+ function() {
+ promise.then(null, error(1, 'oops', true)).
+ then(success(2), error(2));
+ syncReject(deferred, 'timeout');
+ expect(logStr()).toBe('error1(timeout); error2(oops)');
+ });
+
+
+ it('should call success callback in the next turn even if promise is already resolved',
+ function() {
+ deferred.resolve('done!');
+
+ promise.then(success());
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(log).toEqual(['success(done!)']);
+ });
+
+
+ it('should call errpr callback in the next turn even if promise is already rejected',
+ function() {
+ deferred.reject('oops!');
+
+ promise.then(null, error());
+ expect(logStr()).toBe('');
+
+ mockNextTick.flush();
+ expect(log).toEqual(['error(oops!)']);
+ });
+ });
+ });
+ });
+
+
+ describe('reject', function() {
+ it('should package a string into a rejected promise', function() {
+ var rejectedPromise = q.reject('not gonna happen');
+ promise.then(success(), error());
+ syncResolve(deferred, rejectedPromise);
+ expect(log).toEqual(['error(not gonna happen)']);
+ });
+
+
+ it('should package an exception into a rejected promise', function() {
+ var rejectedPromise = q.reject(Error('not gonna happen'));
+ promise.then(success(), error());
+ syncResolve(deferred, rejectedPromise);
+ expect(log).toEqual(['error(Error: not gonna happen)']);
+ });
+ });
+
+
+ describe('when', function() {
+ describe('resolution', function() {
+ it('should call the success callback in the next turn when the value is a non-promise',
+ function() {
+ q.when('hello', success(), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success(hello)');
+ });
+
+
+ it('should call the success callback in the next turn when the value is a resolved promise',
+ function() {
+ deferred.resolve('hello');
+ q.when(deferred.promise, success(), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success(hello)');
+ });
+
+
+ it('should call the errback in the next turn when the value is a rejected promise', function() {
+ deferred.reject('nope');
+ q.when(deferred.promise, success(), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error(nope)');
+ });
+
+
+ it('should call the success callback after the original promise is resolved',
+ function() {
+ q.when(deferred.promise, success(), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ syncResolve(deferred, 'hello');
+ expect(logStr()).toBe('success(hello)');
+ });
+
+
+ it('should call the errback after the orignal promise is rejected',
+ function() {
+ q.when(deferred.promise, success(), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ syncReject(deferred, 'nope');
+ expect(logStr()).toBe('error(nope)');
+ });
+ });
+
+
+ describe('optional callbacks', function() {
+ it('should not require success callback and propagate resolution', function() {
+ q.when('hi', null, error()).then(success(2), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success2(hi)');
+ });
+
+
+ it('should not require success callback and propagate rejection', function() {
+ q.when(q.reject('sorry'), null, error(1)).then(success(), error(2));
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(sorry); error2(sorry)');
+ });
+
+
+ it('should not require errback and propagate resolution', function() {
+ q.when('hi', success(1, 'hello')).then(success(2), error());
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(hi); success2(hello)');
+ });
+
+
+ it('should not require errback and propagate rejection', function() {
+ q.when(q.reject('sorry'), success()).then(success(2), error(2));
+ expect(logStr()).toBe('');
+ mockNextTick.flush();
+ expect(logStr()).toBe('error2(sorry)');
+ });
+ });
+
+
+ describe('returned promise', function() {
+ it('should return a promise that can be resolved with a value returned from the success ' +
+ 'callback', function() {
+ q.when('hello', success(1, 'hi'), error()).then(success(2), error());
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(hello); success2(hi)');
+ });
+
+
+ it('should return a promise that can be rejected with a rejected promise returned from the ' +
+ 'success callback', function() {
+ q.when('hello', success(1, q.reject('sorry')), error()).then(success(), error(2));
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(hello); error2(sorry)');
+ });
+
+
+ it('should return a promise that can be resolved with a value returned from the errback',
+ function() {
+ q.when(q.reject('sorry'), success(), error(1, 'hi')).then(success(2), error());
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(sorry); success2(hi)');
+ });
+
+
+ it('should return a promise that can be rejected with a rejected promise returned from the ' +
+ 'errback', function() {
+ q.when(q.reject('sorry'), success(), error(1, q.reject('sigh'))).then(success(), error(2));
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(sorry); error2(sigh)');
+ });
+
+
+ it('should return a promise that can be resolved with a promise returned from the success ' +
+ 'callback', function() {
+ var deferred2 = defer();
+ q.when('hi', success(1, deferred2.promise), error()).then(success(2), error());
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(hi)');
+ syncResolve(deferred2, 'finally!');
+ expect(logStr()).toBe('success1(hi); success2(finally!)');
+ });
+
+
+ it('should return a promise that can be resolved with promise returned from the errback ' +
+ 'callback', function() {
+ var deferred2 = defer();
+ q.when(q.reject('sorry'), success(), error(1, deferred2.promise)).then(success(2), error());
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(sorry)');
+ syncResolve(deferred2, 'finally!');
+ expect(logStr()).toBe('error1(sorry); success2(finally!)');
+ });
+ });
+
+
+ describe('security', function() {
+ it('should call success callback only once even if the original promise gets fullfilled ' +
+ 'multiple times', function() {
+ var evilPromise = {
+ then: function(success, error) {
+ evilPromise.success = success;
+ evilPromise.error = error;
+ }
+ }
+
+ q.when(evilPromise, success(), error());
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ evilPromise.success('done');
+ mockNextTick.flush(); // TODO(i) wrong queue, evil promise would be resolved outside of the
+ // scope.$apply lifecycle and in that case we should have some kind
+ // of fallback queue for calling our callbacks from. Otherwise the
+ // application will get stuck until something triggers next $apply.
+ expect(logStr()).toBe('success(done)');
+
+ evilPromise.success('evil is me');
+ evilPromise.error('burn burn');
+ expect(logStr()).toBe('success(done)');
+ });
+
+
+ it('should call errback only once even if the original promise gets fullfilled multiple ' +
+ 'times', function() {
+ var evilPromise = {
+ then: function(success, error) {
+ evilPromise.success = success;
+ evilPromise.error = error;
+ }
+ }
+
+ q.when(evilPromise, success(), error());
+ mockNextTick.flush();
+ expect(logStr()).toBe('');
+ evilPromise.error('failed');
+ expect(logStr()).toBe('error(failed)');
+
+ evilPromise.error('muhaha');
+ evilPromise.success('take this');
+ expect(logStr()).toBe('error(failed)');
+ });
+ });
+ });
+
+
+ describe('all', function() {
+ it('should resolve all of nothing', function() {
+ var result;
+ q.all([]).then(function(r) { result = r; });
+ mockNextTick.flush();
+ expect(result).toEqual([]);
+ });
+
+
+ it('should take an array of promises and return a promise for an array of results', function() {
+ var deferred1 = defer(),
+ deferred2 = defer();
+
+ q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error());
+ expect(logStr()).toBe('');
+ syncResolve(deferred, 'hi');
+ expect(logStr()).toBe('');
+ syncResolve(deferred2, 'cau');
+ expect(logStr()).toBe('');
+ syncResolve(deferred1, 'hola');
+ expect(logStr()).toBe('success([hi,hola,cau])');
+ });
+
+
+ it('should reject the derived promise if at least one of the promises in the array is rejected',
+ function() {
+ var deferred1 = defer(),
+ deferred2 = defer();
+
+ q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error());
+ expect(logStr()).toBe('');
+ syncResolve(deferred2, 'cau');
+ expect(logStr()).toBe('');
+ syncReject(deferred1, 'oops');
+ expect(logStr()).toBe('error(oops)');
+ });
+
+
+ it('should ignore multiple resolutions of an (evil) array promise', function() {
+ var evilPromise = {
+ then: function(success, error) {
+ evilPromise.success = success;
+ evilPromise.error = error;
+ }
+ }
+
+ q.all([promise, evilPromise]).then(success(), error());
+ expect(logStr()).toBe('');
+
+ evilPromise.success('first');
+ evilPromise.success('muhaha');
+ evilPromise.error('arghhh');
+ expect(logStr()).toBe('');
+
+ syncResolve(deferred, 'done');
+ expect(logStr()).toBe('success([done,first])');
+ });
+ });
+
+
+ describe('exception logging', function() {
+ var mockExceptionLogger = {
+ log: [],
+ logger: function(e) {
+ mockExceptionLogger.log.push(e);
+ }
+ }
+
+
+ beforeEach(function() {
+ q = qFactory(mockNextTick.nextTick, mockExceptionLogger.logger),
+ defer = q.defer;
+ deferred = defer()
+ promise = deferred.promise;
+ log = [];
+ mockExceptionLogger.log = [];
+ });
+
+
+ describe('in then', function() {
+ it('should log exceptions thrown in a success callback and reject the derived promise',
+ function() {
+ var success1 = success(1, 'oops', true);
+ promise.then(success1).then(success(2), error(2));
+ syncResolve(deferred, 'done');
+ expect(logStr()).toBe('success1(done); error2(oops)');
+ expect(mockExceptionLogger.log).toEqual(['oops']);
+ });
+
+
+ it('should NOT log exceptions when a success callback returns rejected promise', function() {
+ promise.then(success(1, q.reject('rejected'))).then(success(2), error(2));
+ syncResolve(deferred, 'done');
+ expect(logStr()).toBe('success1(done); error2(rejected)');
+ expect(mockExceptionLogger.log).toEqual([]);
+ });
+
+
+ it('should log exceptions thrown in a errback and reject the derived promise', function() {
+ var error1 = error(1, 'oops', true);
+ promise.then(null, error1).then(success(2), error(2));
+ syncReject(deferred, 'nope');
+ expect(logStr()).toBe('error1(nope); error2(oops)');
+ expect(mockExceptionLogger.log).toEqual(['oops']);
+ });
+
+
+ it('should NOT log exceptions when an errback returns a rejected promise', function() {
+ promise.then(null, error(1, q.reject('rejected'))).then(success(2), error(2));
+ syncReject(deferred, 'nope');
+ expect(logStr()).toBe('error1(nope); error2(rejected)');
+ expect(mockExceptionLogger.log).toEqual([]);
+ });
+ });
+
+
+ describe('in when', function() {
+ it('should log exceptions thrown in a success callback and reject the derived promise',
+ function() {
+ var success1 = success(1, 'oops', true);
+ q.when('hi', success1, error()).then(success(), error(2));
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(hi); error2(oops)');
+ expect(mockExceptionLogger.log).toEqual(['oops']);
+ });
+
+
+ it('should NOT log exceptions when a success callback returns rejected promise', function() {
+ q.when('hi', success(1, q.reject('rejected'))).then(success(2), error(2));
+ mockNextTick.flush();
+ expect(logStr()).toBe('success1(hi); error2(rejected)');
+ expect(mockExceptionLogger.log).toEqual([]);
+ });
+
+
+ it('should log exceptions thrown in a errback and reject the derived promise', function() {
+ var error1 = error(1, 'oops', true);
+ q.when(q.reject('sorry'), success(), error1).then(success(), error(2));
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(sorry); error2(oops)');
+ expect(mockExceptionLogger.log).toEqual(['oops']);
+ });
+
+
+ it('should NOT log exceptions when an errback returns a rejected promise', function() {
+ q.when(q.reject('sorry'), success(), error(1, q.reject('rejected'))).
+ then(success(2), error(2));
+ mockNextTick.flush();
+ expect(logStr()).toBe('error1(sorry); error2(rejected)');
+ expect(mockExceptionLogger.log).toEqual([]);
+ });
+ });
+ });
+});
diff --git a/test/ng/resourceSpec.js b/test/ng/resourceSpec.js
new file mode 100644
index 00000000..e0049761
--- /dev/null
+++ b/test/ng/resourceSpec.js
@@ -0,0 +1,325 @@
+'use strict';
+
+describe("resource", function() {
+ var $resource, CreditCard, callback, $httpBackend;
+
+ beforeEach(inject(function($injector) {
+ $httpBackend = $injector.get('$httpBackend');
+ $resource = $injector.get('$resource');
+ CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, {
+ charge:{
+ method:'POST',
+ params:{verb:'!charge'}
+ }
+ });
+ callback = jasmine.createSpy();
+ }));
+
+
+ afterEach(function() {
+ $httpBackend.verifyNoOutstandingExpectation();
+ });
+
+
+ it("should build resource", function() {
+ expect(typeof CreditCard).toBe('function');
+ expect(typeof CreditCard.get).toBe('function');
+ expect(typeof CreditCard.save).toBe('function');
+ expect(typeof CreditCard.remove).toBe('function');
+ expect(typeof CreditCard['delete']).toBe('function');
+ expect(typeof CreditCard.query).toBe('function');
+ });
+
+
+ it('should default to empty parameters', function() {
+ $httpBackend.expect('GET', 'URL').respond({});
+ $resource('URL').query();
+ });
+
+
+ it('should ignore slashes of undefinend parameters', function() {
+ var R = $resource('/Path/:a/:b/:c');
+
+ $httpBackend.when('GET').respond('{}');
+ $httpBackend.expect('GET', '/Path');
+ $httpBackend.expect('GET', '/Path/1');
+ $httpBackend.expect('GET', '/Path/2/3');
+ $httpBackend.expect('GET', '/Path/4/5/6');
+
+ R.get({});
+ R.get({a:1});
+ R.get({a:2, b:3});
+ R.get({a:4, b:5, c:6});
+ });
+
+
+ it('should support escaping colons in url template', function() {
+ var R = $resource('http://localhost\\:8080/Path/:a/\\:stillPath/:b');
+
+ $httpBackend.expect('GET', 'http://localhost:8080/Path/foo/:stillPath/bar').respond();
+ R.get({a: 'foo', b: 'bar'});
+ });
+
+
+ it('should correctly encode url params', function() {
+ var R = $resource('/Path/:a');
+
+ $httpBackend.expect('GET', '/Path/foo%231').respond('{}');
+ $httpBackend.expect('GET', '/Path/doh!@foo?bar=baz%231').respond('{}');
+
+ R.get({a: 'foo#1'});
+ R.get({a: 'doh!@foo', bar: 'baz#1'});
+ });
+
+
+ it('should not encode @ in url params', function() {
+ //encodeURIComponent is too agressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt
+ //with regards to the character set (pchar) allowed in path segments
+ //so we need this test to make sure that we don't over-encode the params and break stuff like
+ //buzz api which uses @self
+
+ var R = $resource('/Path/:a');
+ $httpBackend.expect('GET', '/Path/doh@fo%20o?!do%26h=g%3Da+h&:bar=$baz@1').respond('{}');
+ R.get({a: 'doh@fo o', ':bar': '$baz@1', '!do&h': 'g=a h'});
+ });
+
+
+ it('should encode & in url params', function() {
+ var R = $resource('/Path/:a');
+ $httpBackend.expect('GET', '/Path/doh&foo?bar=baz%261').respond('{}');
+ R.get({a: 'doh&foo', bar: 'baz&1'});
+ });
+
+
+ it('should build resource with default param', function() {
+ $httpBackend.expect('GET', '/Order/123/Line/456.visa?minimum=0.05').respond({id: 'abc'});
+ var LineItem = $resource('/Order/:orderId/Line/:id:verb',
+ {orderId: '123', id: '@id.key', verb:'.visa', minimum: 0.05});
+ var item = LineItem.get({id: 456});
+ $httpBackend.flush();
+ expect(item).toEqualData({id:'abc'});
+ });
+
+
+ it("should build resource with action default param overriding default param", function() {
+ $httpBackend.expect('GET', '/Customer/123').respond({id: 'abc'});
+ var TypeItem = $resource('/:type/:typeId', {type: 'Order'},
+ {get: {method: 'GET', params: {type: 'Customer'}}});
+ var item = TypeItem.get({typeId: 123});
+
+ $httpBackend.flush();
+ expect(item).toEqualData({id: 'abc'});
+ });
+
+
+ it("should create resource", function() {
+ $httpBackend.expect('POST', '/CreditCard', '{"name":"misko"}').respond({id: 123, name: 'misko'});
+
+ var cc = CreditCard.save({name: 'misko'}, callback);
+ expect(cc).toEqualData({name: 'misko'});
+ expect(callback).not.toHaveBeenCalled();
+
+ $httpBackend.flush();
+ expect(cc).toEqualData({id: 123, name: 'misko'});
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback.mostRecentCall.args[0]).toEqual(cc);
+ expect(callback.mostRecentCall.args[1]()).toEqual({});
+ });
+
+
+ it("should read resource", function() {
+ $httpBackend.expect('GET', '/CreditCard/123').respond({id: 123, number: '9876'});
+ var cc = CreditCard.get({id: 123}, callback);
+
+ expect(cc instanceof CreditCard).toBeTruthy();
+ expect(cc).toEqualData({});
+ expect(callback).not.toHaveBeenCalled();
+
+ $httpBackend.flush();
+ expect(cc).toEqualData({id: 123, number: '9876'});
+ expect(callback.mostRecentCall.args[0]).toEqual(cc);
+ expect(callback.mostRecentCall.args[1]()).toEqual({});
+ });
+
+
+ it("should read partial resource", function() {
+ $httpBackend.expect('GET', '/CreditCard').respond([{id:{key:123}}]);
+ var ccs = CreditCard.query();
+
+ $httpBackend.flush();
+ expect(ccs.length).toEqual(1);
+
+ var cc = ccs[0];
+ expect(cc instanceof CreditCard).toBe(true);
+ expect(cc.number).toBeUndefined();
+
+ $httpBackend.expect('GET', '/CreditCard/123').respond({id: {key: 123}, number: '9876'});
+ cc.$get(callback);
+ $httpBackend.flush();
+ expect(callback.mostRecentCall.args[0]).toEqual(cc);
+ expect(callback.mostRecentCall.args[1]()).toEqual({});
+ expect(cc.number).toEqual('9876');
+ });
+
+
+ it("should update resource", function() {
+ $httpBackend.expect('POST', '/CreditCard/123', '{"id":{"key":123},"name":"misko"}').
+ respond({id: {key: 123}, name: 'rama'});
+
+ var cc = CreditCard.save({id: {key: 123}, name: 'misko'}, callback);
+ expect(cc).toEqualData({id:{key:123}, name:'misko'});
+ expect(callback).not.toHaveBeenCalled();
+ $httpBackend.flush();
+ });
+
+
+ it("should query resource", function() {
+ $httpBackend.expect('GET', '/CreditCard?key=value').respond([{id: 1}, {id: 2}]);
+
+ var ccs = CreditCard.query({key: 'value'}, callback);
+ expect(ccs).toEqual([]);
+ expect(callback).not.toHaveBeenCalled();
+
+ $httpBackend.flush();
+ expect(ccs).toEqualData([{id:1}, {id:2}]);
+ expect(callback.mostRecentCall.args[0]).toEqual(ccs);
+ expect(callback.mostRecentCall.args[1]()).toEqual({});
+ });
+
+
+ it("should have all arguments optional", function() {
+ $httpBackend.expect('GET', '/CreditCard').respond([{id:1}]);
+
+ var log = '';
+ var ccs = CreditCard.query(function() { log += 'cb;'; });
+
+ $httpBackend.flush();
+ expect(ccs).toEqualData([{id:1}]);
+ expect(log).toEqual('cb;');
+ });
+
+
+ it('should delete resource and call callback', function() {
+ $httpBackend.expect('DELETE', '/CreditCard/123').respond({});
+ CreditCard.remove({id:123}, callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ $httpBackend.flush();
+ expect(callback.mostRecentCall.args[0]).toEqualData({});
+ expect(callback.mostRecentCall.args[1]()).toEqual({});
+
+ callback.reset();
+ $httpBackend.expect('DELETE', '/CreditCard/333').respond(204, null);
+ CreditCard.remove({id:333}, callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ $httpBackend.flush();
+ expect(callback.mostRecentCall.args[0]).toEqualData({});
+ expect(callback.mostRecentCall.args[1]()).toEqual({});
+ });
+
+
+ it('should post charge verb', function() {
+ $httpBackend.expect('POST', '/CreditCard/123!charge?amount=10', '{"auth":"abc"}').respond({success: 'ok'});
+ CreditCard.charge({id:123, amount:10}, {auth:'abc'}, callback);
+ });
+
+
+ it('should post charge verb on instance', function() {
+ $httpBackend.expect('POST', '/CreditCard/123!charge?amount=10',
+ '{"id":{"key":123},"name":"misko"}').respond({success: 'ok'});
+
+ var card = new CreditCard({id:{key:123}, name:'misko'});
+ card.$charge({amount:10}, callback);
+ });
+
+
+ it('should create on save', function() {
+ $httpBackend.expect('POST', '/CreditCard', '{"name":"misko"}').respond({id: 123}, {header1: 'a'});
+
+ var cc = new CreditCard();
+ expect(cc.$get).toBeDefined();
+ expect(cc.$query).toBeDefined();
+ expect(cc.$remove).toBeDefined();
+ expect(cc.$save).toBeDefined();
+
+ cc.name = 'misko';
+ cc.$save(callback);
+ expect(cc).toEqualData({name:'misko'});
+
+ $httpBackend.flush();
+ expect(cc).toEqualData({id:123});
+ expect(callback.mostRecentCall.args[0]).toEqual(cc);
+ expect(callback.mostRecentCall.args[1]()).toEqual({header1: 'a'});
+ });
+
+
+ it('should not mutate the resource object if response contains no body', function() {
+ var data = {id:{key:123}, number:'9876'};
+ $httpBackend.expect('GET', '/CreditCard/123').respond(data);
+
+ var cc = CreditCard.get({id:123});
+ $httpBackend.flush();
+ expect(cc instanceof CreditCard).toBe(true);
+
+ $httpBackend.expect('POST', '/CreditCard/123', toJson(data)).respond('');
+ var idBefore = cc.id;
+
+ cc.$save();
+ $httpBackend.flush();
+ expect(idBefore).toEqual(cc.id);
+ });
+
+
+ it('should bind default parameters', function() {
+ $httpBackend.expect('GET', '/CreditCard/123.visa?minimum=0.05').respond({id: 123});
+ var Visa = CreditCard.bind({verb:'.visa', minimum:0.05});
+ var visa = Visa.get({id:123});
+ $httpBackend.flush();
+ expect(visa).toEqualData({id:123});
+ });
+
+
+ it('should exercise full stack', function() {
+ var Person = $resource('/Person/:id');
+
+ $httpBackend.expect('GET', '/Person/123').respond('\n{\n"name":\n"misko"\n}\n');
+ var person = Person.get({id:123});
+ $httpBackend.flush();
+ expect(person.name).toEqual('misko');
+ });
+
+
+ describe('failure mode', function() {
+ var ERROR_CODE = 500,
+ ERROR_RESPONSE = 'Server Error',
+ errorCB;
+
+ beforeEach(function() {
+ errorCB = jasmine.createSpy('error').andCallFake(function(response) {
+ expect(response.data).toBe(ERROR_RESPONSE);
+ expect(response.status).toBe(ERROR_CODE);
+ });
+ });
+
+
+ it('should call the error callback if provided on non 2xx response', function() {
+ $httpBackend.expect('GET', '/CreditCard/123').respond(ERROR_CODE, ERROR_RESPONSE);
+
+ CreditCard.get({id:123}, callback, errorCB);
+ $httpBackend.flush();
+ expect(errorCB).toHaveBeenCalledOnce();
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+
+ it('should call the error callback if provided on non 2xx response', function() {
+ $httpBackend.expect('GET', '/CreditCard').respond(ERROR_CODE, ERROR_RESPONSE);
+
+ CreditCard.get(callback, errorCB);
+ $httpBackend.flush();
+ expect(errorCB).toHaveBeenCalledOnce();
+ expect(callback).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js
new file mode 100644
index 00000000..35be7a2f
--- /dev/null
+++ b/test/ng/rootScopeSpec.js
@@ -0,0 +1,838 @@
+'use strict';
+
+describe('Scope', function() {
+
+ beforeEach(module(provideLog));
+
+
+ describe('$root', function() {
+ it('should point to itself', inject(function($rootScope) {
+ expect($rootScope.$root).toEqual($rootScope);
+ expect($rootScope.hasOwnProperty('$root')).toBeTruthy();
+ }));
+
+
+ it('should not have $root on children, but should inherit', inject(function($rootScope) {
+ var child = $rootScope.$new();
+ expect(child.$root).toEqual($rootScope);
+ expect(child.hasOwnProperty('$root')).toBeFalsy();
+ }));
+
+ });
+
+
+ describe('$parent', function() {
+ it('should point to itself in root', inject(function($rootScope) {
+ expect($rootScope.$root).toEqual($rootScope);
+ }));
+
+
+ it('should point to parent', inject(function($rootScope) {
+ var child = $rootScope.$new();
+ expect($rootScope.$parent).toEqual(null);
+ expect(child.$parent).toEqual($rootScope);
+ expect(child.$new().$parent).toEqual(child);
+ }));
+ });
+
+
+ describe('$id', function() {
+ it('should have a unique id', inject(function($rootScope) {
+ expect($rootScope.$id < $rootScope.$new().$id).toBeTruthy();
+ }));
+ });
+
+
+ describe('this', function() {
+ it('should have a \'this\'', inject(function($rootScope) {
+ expect($rootScope['this']).toEqual($rootScope);
+ }));
+ });
+
+
+ describe('$new()', function() {
+ it('should create a child scope', inject(function($rootScope) {
+ var child = $rootScope.$new();
+ $rootScope.a = 123;
+ expect(child.a).toEqual(123);
+ }));
+
+ it('should create a non prototypically inherited child scope', inject(function($rootScope) {
+ var child = $rootScope.$new(true);
+ $rootScope.a = 123;
+ expect(child.a).toBeUndefined();
+ expect(child.$parent).toEqual($rootScope);
+ expect(child.$new).toBe($rootScope.$new);
+ expect(child.$root).toBe($rootScope);
+ }));
+ });
+
+
+ describe('$watch/$digest', function() {
+ it('should watch and fire on simple property change', inject(function($rootScope) {
+ var spy = jasmine.createSpy();
+ $rootScope.$watch('name', spy);
+ $rootScope.$digest();
+ spy.reset();
+
+ expect(spy).not.wasCalled();
+ $rootScope.$digest();
+ expect(spy).not.wasCalled();
+ $rootScope.name = 'misko';
+ $rootScope.$digest();
+ expect(spy).wasCalledWith('misko', undefined, $rootScope);
+ }));
+
+
+ it('should watch and fire on expression change', inject(function($rootScope) {
+ var spy = jasmine.createSpy();
+ $rootScope.$watch('name.first', spy);
+ $rootScope.$digest();
+ spy.reset();
+
+ $rootScope.name = {};
+ expect(spy).not.wasCalled();
+ $rootScope.$digest();
+ expect(spy).not.wasCalled();
+ $rootScope.name.first = 'misko';
+ $rootScope.$digest();
+ expect(spy).wasCalled();
+ }));
+
+
+ it('should delegate exceptions', function() {
+ module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ });
+ inject(function($rootScope, $exceptionHandler, $log) {
+ $rootScope.$watch('a', function() {throw new Error('abc');});
+ $rootScope.a = 1;
+ $rootScope.$digest();
+ expect($exceptionHandler.errors[0].message).toEqual('abc');
+ $log.assertEmpty();
+ });
+ });
+
+
+ it('should fire watches in order of addition', inject(function($rootScope) {
+ // this is not an external guarantee, just our own sanity
+ var log = '';
+ $rootScope.$watch('a', function() { log += 'a'; });
+ $rootScope.$watch('b', function() { log += 'b'; });
+ $rootScope.$watch('c', function() { log += 'c'; });
+ $rootScope.a = $rootScope.b = $rootScope.c = 1;
+ $rootScope.$digest();
+ expect(log).toEqual('abc');
+ }));
+
+
+ it('should call child $watchers in addition order', inject(function($rootScope) {
+ // this is not an external guarantee, just our own sanity
+ var log = '';
+ var childA = $rootScope.$new();
+ var childB = $rootScope.$new();
+ var childC = $rootScope.$new();
+ childA.$watch('a', function() { log += 'a'; });
+ childB.$watch('b', function() { log += 'b'; });
+ childC.$watch('c', function() { log += 'c'; });
+ childA.a = childB.b = childC.c = 1;
+ $rootScope.$digest();
+ expect(log).toEqual('abc');
+ }));
+
+
+ it('should allow $digest on a child scope with and without a right sibling', inject(
+ function($rootScope) {
+ // tests a traversal edge case which we originally missed
+ var log = '',
+ childA = $rootScope.$new(),
+ childB = $rootScope.$new();
+
+ $rootScope.$watch(function() { log += 'r'; });
+ childA.$watch(function() { log += 'a'; });
+ childB.$watch(function() { log += 'b'; });
+
+ // init
+ $rootScope.$digest();
+ expect(log).toBe('rabrab');
+
+ log = '';
+ childA.$digest();
+ expect(log).toBe('a');
+
+ log = '';
+ childB.$digest();
+ expect(log).toBe('b');
+ }));
+
+
+ it('should repeat watch cycle while model changes are identified', inject(function($rootScope) {
+ var log = '';
+ $rootScope.$watch('c', function(v) {$rootScope.d = v; log+='c'; });
+ $rootScope.$watch('b', function(v) {$rootScope.c = v; log+='b'; });
+ $rootScope.$watch('a', function(v) {$rootScope.b = v; log+='a'; });
+ $rootScope.$digest();
+ log = '';
+ $rootScope.a = 1;
+ $rootScope.$digest();
+ expect($rootScope.b).toEqual(1);
+ expect($rootScope.c).toEqual(1);
+ expect($rootScope.d).toEqual(1);
+ expect(log).toEqual('abc');
+ }));
+
+
+ it('should repeat watch cycle from the root elemnt', inject(function($rootScope) {
+ var log = '';
+ var child = $rootScope.$new();
+ $rootScope.$watch(function() { log += 'a'; });
+ child.$watch(function() { log += 'b'; });
+ $rootScope.$digest();
+ expect(log).toEqual('abab');
+ }));
+
+
+ it('should prevent infinite recursion and print watcher expression',function() {
+ module(function($rootScopeProvider) {
+ $rootScopeProvider.digestTtl(100);
+ });
+ inject(function($rootScope) {
+ $rootScope.$watch('a', function() {$rootScope.b++;});
+ $rootScope.$watch('b', function() {$rootScope.a++;});
+ $rootScope.a = $rootScope.b = 0;
+
+ expect(function() {
+ $rootScope.$digest();
+ }).toThrow('100 $digest() iterations reached. Aborting!\n'+
+ 'Watchers fired in the last 5 iterations: ' +
+ '[["a; newVal: 96; oldVal: 95","b; newVal: 97; oldVal: 96"],' +
+ '["a; newVal: 97; oldVal: 96","b; newVal: 98; oldVal: 97"],' +
+ '["a; newVal: 98; oldVal: 97","b; newVal: 99; oldVal: 98"],' +
+ '["a; newVal: 99; oldVal: 98","b; newVal: 100; oldVal: 99"],' +
+ '["a; newVal: 100; oldVal: 99","b; newVal: 101; oldVal: 100"]]');
+ });
+ });
+
+
+ it('should prevent infinite recursion and print print watcher function name or body',
+ inject(function($rootScope) {
+ $rootScope.$watch(function watcherA() {return $rootScope.a;}, function() {$rootScope.b++;});
+ $rootScope.$watch(function() {return $rootScope.b;}, function() {$rootScope.a++;});
+ $rootScope.a = $rootScope.b = 0;
+
+ try {
+ $rootScope.$digest();
+ throw Error('Should have thrown exception');
+ } catch(e) {
+ expect(e.message.match(/"fn: (watcherA|function)/g).length).toBe(10);
+ }
+ }));
+
+
+ it('should not fire upon $watch registration on initial $digest', inject(function($rootScope) {
+ var log = '';
+ $rootScope.a = 1;
+ $rootScope.$watch('a', function() { log += 'a'; });
+ $rootScope.$watch('b', function() { log += 'b'; });
+ $rootScope.$digest();
+ log = '';
+ $rootScope.$digest();
+ expect(log).toEqual('');
+ }));
+
+
+ it('should watch objects', inject(function($rootScope) {
+ var log = '';
+ $rootScope.a = [];
+ $rootScope.b = {};
+ $rootScope.$watch('a', function(value) {
+ log +='.';
+ expect(value).toBe($rootScope.a);
+ }, true);
+ $rootScope.$watch('b', function(value) {
+ log +='!';
+ expect(value).toBe($rootScope.b);
+ }, true);
+ $rootScope.$digest();
+ log = '';
+
+ $rootScope.a.push({});
+ $rootScope.b.name = '';
+
+ $rootScope.$digest();
+ expect(log).toEqual('.!');
+ }));
+
+
+ it('should watch functions', function() {
+ module(provideLog);
+ inject(function($rootScope, log) {
+ $rootScope.fn = function() {return 'a'};
+ $rootScope.$watch('fn', function(fn) {
+ log(fn());
+ });
+ $rootScope.$digest();
+ expect(log).toEqual('a');
+ $rootScope.fn = function() {return 'b'};
+ $rootScope.$digest();
+ expect(log).toEqual('a; b');
+ })
+ });
+
+
+ it('should prevent $digest recursion', inject(function($rootScope) {
+ var callCount = 0;
+ $rootScope.$watch('name', function() {
+ expect(function() {
+ $rootScope.$digest();
+ }).toThrow('$digest already in progress');
+ callCount++;
+ });
+ $rootScope.name = 'a';
+ $rootScope.$digest();
+ expect(callCount).toEqual(1);
+ }));
+
+
+ it('should return a function that allows listeners to be unregistered', inject(
+ function($rootScope) {
+ var listener = jasmine.createSpy('watch listener'),
+ listenerRemove;
+
+ listenerRemove = $rootScope.$watch('foo', listener);
+ $rootScope.$digest(); //init
+ expect(listener).toHaveBeenCalled();
+ expect(listenerRemove).toBeDefined();
+
+ listener.reset();
+ $rootScope.foo = 'bar';
+ $rootScope.$digest(); //triger
+ expect(listener).toHaveBeenCalledOnce();
+
+ listener.reset();
+ $rootScope.foo = 'baz';
+ listenerRemove();
+ $rootScope.$digest(); //trigger
+ expect(listener).not.toHaveBeenCalled();
+ }));
+
+
+ it('should not infinitely digest when current value is NaN', inject(function($rootScope) {
+ $rootScope.$watch(function() { return NaN;});
+
+ expect(function() {
+ $rootScope.$digest();
+ }).not.toThrow();
+ }));
+
+
+ it('should always call the watchr with newVal and oldVal equal on the first run',
+ inject(function($rootScope) {
+ var log = [];
+ function logger(scope, newVal, oldVal) {
+ var val = (newVal === oldVal || (newVal !== oldVal && oldVal !== newVal)) ? newVal : 'xxx';
+ log.push(val);
+ }
+
+ $rootScope.$watch(function() { return NaN;}, logger);
+ $rootScope.$watch(function() { return undefined;}, logger);
+ $rootScope.$watch(function() { return '';}, logger);
+ $rootScope.$watch(function() { return false;}, logger);
+ $rootScope.$watch(function() { return {};}, logger, true);
+ $rootScope.$watch(function() { return 23;}, logger);
+
+ $rootScope.$digest();
+ expect(isNaN(log.shift())).toBe(true); //jasmine's toBe and toEqual don't work well with NaNs
+ expect(log).toEqual([undefined, '', false, {}, 23]);
+ log = [];
+ $rootScope.$digest();
+ expect(log).toEqual([]);
+ }));
+ });
+
+
+ describe('$destroy', function() {
+ var first = null, middle = null, last = null, log = null;
+
+ beforeEach(inject(function($rootScope) {
+ log = '';
+
+ first = $rootScope.$new();
+ middle = $rootScope.$new();
+ last = $rootScope.$new();
+
+ first.$watch(function() { log += '1';});
+ middle.$watch(function() { log += '2';});
+ last.$watch(function() { log += '3';});
+
+ $rootScope.$digest();
+ log = '';
+ }));
+
+
+ it('should ignore remove on root', inject(function($rootScope) {
+ $rootScope.$destroy();
+ $rootScope.$digest();
+ expect(log).toEqual('123');
+ }));
+
+
+ it('should remove first', inject(function($rootScope) {
+ first.$destroy();
+ $rootScope.$digest();
+ expect(log).toEqual('23');
+ }));
+
+
+ it('should remove middle', inject(function($rootScope) {
+ middle.$destroy();
+ $rootScope.$digest();
+ expect(log).toEqual('13');
+ }));
+
+
+ it('should remove last', inject(function($rootScope) {
+ last.$destroy();
+ $rootScope.$digest();
+ expect(log).toEqual('12');
+ }));
+
+
+ it('should broadcast the $destroy event', inject(function($rootScope, log) {
+ first.$on('$destroy', log.fn('first'));
+ first.$new().$on('$destroy', log.fn('first-child'));
+
+ first.$destroy();
+ expect(log).toEqual('first; first-child');
+ }));
+ });
+
+
+ describe('$eval', function() {
+ it('should eval an expression', inject(function($rootScope) {
+ expect($rootScope.$eval('a=1')).toEqual(1);
+ expect($rootScope.a).toEqual(1);
+
+ $rootScope.$eval(function(self) {self.b=2;});
+ expect($rootScope.b).toEqual(2);
+ }));
+
+
+ it('should allow passing locals to the expression', inject(function($rootScope) {
+ expect($rootScope.$eval('a+1', {a: 2})).toBe(3);
+
+ $rootScope.$eval(function(scope, locals) {
+ scope.c = locals.b + 4;
+ }, {b: 3});
+ expect($rootScope.c).toBe(7);
+ }));
+ });
+
+
+ describe('$evalAsync', function() {
+
+ it('should run callback before $watch', inject(function($rootScope) {
+ var log = '';
+ var child = $rootScope.$new();
+ $rootScope.$evalAsync(function(scope) { log += 'parent.async;'; });
+ $rootScope.$watch('value', function() { log += 'parent.$digest;'; });
+ child.$evalAsync(function(scope) { log += 'child.async;'; });
+ child.$watch('value', function() { log += 'child.$digest;'; });
+ $rootScope.$digest();
+ expect(log).toEqual('parent.async;parent.$digest;child.async;child.$digest;');
+ }));
+
+ it('should cause a $digest rerun', inject(function($rootScope) {
+ $rootScope.log = '';
+ $rootScope.value = 0;
+ $rootScope.$watch('value', 'log = log + ".";');
+ $rootScope.$watch('init', function() {
+ $rootScope.$evalAsync('value = 123; log = log + "=" ');
+ expect($rootScope.value).toEqual(0);
+ });
+ $rootScope.$digest();
+ expect($rootScope.log).toEqual('.=.');
+ }));
+
+ it('should run async in the same order as added', inject(function($rootScope) {
+ $rootScope.log = '';
+ $rootScope.$evalAsync("log = log + 1");
+ $rootScope.$evalAsync("log = log + 2");
+ $rootScope.$digest();
+ expect($rootScope.log).toBe('12');
+ }));
+
+ });
+
+
+ describe('$apply', function() {
+ it('should apply expression with full lifecycle', inject(function($rootScope) {
+ var log = '';
+ var child = $rootScope.$new();
+ $rootScope.$watch('a', function(a) { log += '1'; });
+ child.$apply('$parent.a=0');
+ expect(log).toEqual('1');
+ }));
+
+
+ it('should catch exceptions', function() {
+ module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ });
+ inject(function($rootScope, $exceptionHandler, $log) {
+ var log = '';
+ var child = $rootScope.$new();
+ $rootScope.$watch('a', function(a) { log += '1'; });
+ $rootScope.a = 0;
+ child.$apply(function() { throw new Error('MyError'); });
+ expect(log).toEqual('1');
+ expect($exceptionHandler.errors[0].message).toEqual('MyError');
+ $log.error.logs.shift();
+ });
+ });
+
+
+ describe('exceptions', function() {
+ var log;
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ }));
+ beforeEach(inject(function($rootScope) {
+ log = '';
+ $rootScope.$watch(function() { log += '$digest;'; });
+ $rootScope.$digest();
+ log = '';
+ }));
+
+
+ it('should execute and return value and update', inject(
+ function($rootScope, $exceptionHandler) {
+ $rootScope.name = 'abc';
+ expect($rootScope.$apply(function(scope) {
+ return scope.name;
+ })).toEqual('abc');
+ expect(log).toEqual('$digest;');
+ expect($exceptionHandler.errors).toEqual([]);
+ }));
+
+
+ it('should catch exception and update', inject(function($rootScope, $exceptionHandler) {
+ var error = new Error('MyError');
+ $rootScope.$apply(function() { throw error; });
+ expect(log).toEqual('$digest;');
+ expect($exceptionHandler.errors).toEqual([error]);
+ }));
+ });
+
+
+ describe('recursive $apply protection', function() {
+ it('should throw an exception if $apply is called while an $apply is in progress', inject(
+ function($rootScope) {
+ expect(function() {
+ $rootScope.$apply(function() {
+ $rootScope.$apply();
+ });
+ }).toThrow('$apply already in progress');
+ }));
+
+
+ it('should throw an exception if $apply is called while flushing evalAsync queue', inject(
+ function($rootScope) {
+ expect(function() {
+ $rootScope.$apply(function() {
+ $rootScope.$evalAsync(function() {
+ $rootScope.$apply();
+ });
+ });
+ }).toThrow('$digest already in progress');
+ }));
+
+
+ it('should throw an exception if $apply is called while a watch is being initialized', inject(
+ function($rootScope) {
+ var childScope1 = $rootScope.$new();
+ childScope1.$watch('x', function() {
+ childScope1.$apply();
+ });
+ expect(function() { childScope1.$apply(); }).toThrow('$digest already in progress');
+ }));
+
+
+ it('should thrown an exception if $apply in called from a watch fn (after init)', inject(
+ function($rootScope) {
+ var childScope2 = $rootScope.$new();
+ childScope2.$apply(function() {
+ childScope2.$watch('x', function(newVal, oldVal) {
+ if (newVal !== oldVal) {
+ childScope2.$apply();
+ }
+ });
+ });
+
+ expect(function() { childScope2.$apply(function() {
+ childScope2.x = 'something';
+ }); }).toThrow('$digest already in progress');
+ }));
+ });
+ });
+
+
+ describe('events', function() {
+
+ describe('$on', function() {
+
+ it('should add listener for both $emit and $broadcast events', inject(function($rootScope) {
+ var log = '',
+ child = $rootScope.$new();
+
+ function eventFn() {
+ log += 'X';
+ }
+
+ child.$on('abc', eventFn);
+ expect(log).toEqual('');
+
+ child.$emit('abc');
+ expect(log).toEqual('X');
+
+ child.$broadcast('abc');
+ expect(log).toEqual('XX');
+ }));
+
+
+ it('should return a function that deregisters the listener', inject(function($rootScope) {
+ var log = '',
+ child = $rootScope.$new(),
+ listenerRemove;
+
+ function eventFn() {
+ log += 'X';
+ }
+
+ listenerRemove = child.$on('abc', eventFn);
+ expect(log).toEqual('');
+ expect(listenerRemove).toBeDefined();
+
+ child.$emit('abc');
+ child.$broadcast('abc');
+ expect(log).toEqual('XX');
+
+ log = '';
+ listenerRemove();
+ child.$emit('abc');
+ child.$broadcast('abc');
+ expect(log).toEqual('');
+ }));
+ });
+
+
+ describe('$emit', function() {
+ var log, child, grandChild, greatGrandChild;
+
+ function logger(event) {
+ log += event.currentScope.id + '>';
+ }
+
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ }));
+ beforeEach(inject(function($rootScope) {
+ log = '';
+ child = $rootScope.$new();
+ grandChild = child.$new();
+ greatGrandChild = grandChild.$new();
+
+ $rootScope.id = 0;
+ child.id = 1;
+ grandChild.id = 2;
+ greatGrandChild.id = 3;
+
+ $rootScope.$on('myEvent', logger);
+ child.$on('myEvent', logger);
+ grandChild.$on('myEvent', logger);
+ greatGrandChild.$on('myEvent', logger);
+ }));
+
+ it('should bubble event up to the root scope', function() {
+ grandChild.$emit('myEvent');
+ expect(log).toEqual('2>1>0>');
+ });
+
+
+ it('should dispatch exceptions to the $exceptionHandler',
+ inject(function($exceptionHandler) {
+ child.$on('myEvent', function() { throw 'bubbleException'; });
+ grandChild.$emit('myEvent');
+ expect(log).toEqual('2>1>0>');
+ expect($exceptionHandler.errors).toEqual(['bubbleException']);
+ }));
+
+
+ it('should allow cancelation of event propagation', function() {
+ child.$on('myEvent', function(event) { event.cancel(); });
+ grandChild.$emit('myEvent');
+ expect(log).toEqual('2>1>');
+ });
+
+
+ it('should forward method arguments', function() {
+ child.$on('abc', function(event, arg1, arg2) {
+ expect(event.name).toBe('abc');
+ expect(arg1).toBe('arg1');
+ expect(arg2).toBe('arg2');
+ });
+ child.$emit('abc', 'arg1', 'arg2');
+ });
+
+
+ it('should return event object with cancelled property', function() {
+ child.$on('some', function(event) {
+ event.cancel();
+ });
+
+ var result = grandChild.$emit('some');
+ expect(result).toBeDefined();
+ expect(result.cancelled).toBe(true);
+ });
+
+
+ describe('event object', function() {
+ it('should have methods/properties', function() {
+ var event;
+ child.$on('myEvent', function(e) {
+ expect(e.targetScope).toBe(grandChild);
+ expect(e.currentScope).toBe(child);
+ expect(e.name).toBe('myEvent');
+ event = e;
+ });
+ grandChild.$emit('myEvent');
+ expect(event).toBeDefined();
+ });
+ });
+ });
+
+
+ describe('$broadcast', function() {
+ describe('event propagation', function() {
+ var log, child1, child2, child3, grandChild11, grandChild21, grandChild22, grandChild23,
+ greatGrandChild211;
+
+ function logger(event) {
+ log += event.currentScope.id + '>';
+ }
+
+ beforeEach(inject(function($rootScope) {
+ log = '';
+ child1 = $rootScope.$new();
+ child2 = $rootScope.$new();
+ child3 = $rootScope.$new();
+ grandChild11 = child1.$new();
+ grandChild21 = child2.$new();
+ grandChild22 = child2.$new();
+ grandChild23 = child2.$new();
+ greatGrandChild211 = grandChild21.$new();
+
+ $rootScope.id = 0;
+ child1.id = 1;
+ child2.id = 2;
+ child3.id = 3;
+ grandChild11.id = 11;
+ grandChild21.id = 21;
+ grandChild22.id = 22;
+ grandChild23.id = 23;
+ greatGrandChild211.id = 211;
+
+ $rootScope.$on('myEvent', logger);
+ child1.$on('myEvent', logger);
+ child2.$on('myEvent', logger);
+ child3.$on('myEvent', logger);
+ grandChild11.$on('myEvent', logger);
+ grandChild21.$on('myEvent', logger);
+ grandChild22.$on('myEvent', logger);
+ grandChild23.$on('myEvent', logger);
+ greatGrandChild211.$on('myEvent', logger);
+
+ // R
+ // / | \
+ // 1 2 3
+ // / / | \
+ // 11 21 22 23
+ // |
+ // 211
+ }));
+
+
+ it('should broadcast an event from the root scope', inject(function($rootScope) {
+ $rootScope.$broadcast('myEvent');
+ expect(log).toBe('0>1>11>2>21>211>22>23>3>');
+ }));
+
+
+ it('should broadcast an event from a child scope', function() {
+ child2.$broadcast('myEvent');
+ expect(log).toBe('2>21>211>22>23>');
+ });
+
+
+ it('should broadcast an event from a leaf scope with a sibling', function() {
+ grandChild22.$broadcast('myEvent');
+ expect(log).toBe('22>');
+ });
+
+
+ it('should broadcast an event from a leaf scope without a sibling', function() {
+ grandChild23.$broadcast('myEvent');
+ expect(log).toBe('23>');
+ });
+
+
+ it('should not not fire any listeners for other events', inject(function($rootScope) {
+ $rootScope.$broadcast('fooEvent');
+ expect(log).toBe('');
+ }));
+
+
+ it('should return event object', function() {
+ var result = child1.$broadcast('some');
+
+ expect(result).toBeDefined();
+ expect(result.name).toBe('some');
+ expect(result.targetScope).toBe(child1);
+ });
+ });
+
+
+ describe('listener', function() {
+ it('should receive event object', inject(function($rootScope) {
+ var scope = $rootScope,
+ child = scope.$new(),
+ event;
+
+ child.$on('fooEvent', function(e) {
+ event = e;
+ });
+ scope.$broadcast('fooEvent');
+
+ expect(event.name).toBe('fooEvent');
+ expect(event.targetScope).toBe(scope);
+ expect(event.currentScope).toBe(child);
+ }));
+
+
+ it('should support passing messages as varargs', inject(function($rootScope) {
+ var scope = $rootScope,
+ child = scope.$new(),
+ args;
+
+ child.$on('fooEvent', function() {
+ args = arguments;
+ });
+ scope.$broadcast('fooEvent', 'do', 're', 'me', 'fa');
+
+ expect(args.length).toBe(5);
+ expect(sliceArgs(args, 1)).toEqual(['do', 're', 'me', 'fa']);
+ }));
+ });
+ });
+ });
+});
diff --git a/test/ng/routeParamsSpec.js b/test/ng/routeParamsSpec.js
new file mode 100644
index 00000000..d1b2ecb1
--- /dev/null
+++ b/test/ng/routeParamsSpec.js
@@ -0,0 +1,20 @@
+'use strict';
+
+describe('$routeParams', function() {
+ it('should publish the params into a service', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo');
+ $routeProvider.when('/bar/:barId');
+ });
+
+ inject(function($rootScope, $route, $location, $routeParams) {
+ $location.path('/foo').search('a=b');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({a:'b'});
+
+ $location.path('/bar/123').search('x=abc');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId:'123', x:'abc'});
+ });
+ });
+});
diff --git a/test/ng/routeSpec.js b/test/ng/routeSpec.js
new file mode 100644
index 00000000..88e54b9a
--- /dev/null
+++ b/test/ng/routeSpec.js
@@ -0,0 +1,471 @@
+'use strict';
+
+describe('$route', function() {
+
+ it('should route and fire change event', function() {
+ var log = '',
+ lastRoute,
+ nextRoute;
+
+ module(function($routeProvider) {
+ $routeProvider.when('/Book/:book/Chapter/:chapter',
+ {controller: noop, template: 'Chapter.html'});
+ $routeProvider.when('/Blank');
+ });
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$beforeRouteChange', function(event, next, current) {
+ log += 'before();';
+ expect(current).toBe($route.current);
+ lastRoute = current;
+ nextRoute = next;
+ });
+ $rootScope.$on('$afterRouteChange', function(event, current, last) {
+ log += 'after();';
+ expect(current).toBe($route.current);
+ expect(lastRoute).toBe(last);
+ expect(nextRoute).toBe(current);
+ });
+
+ $location.path('/Book/Moby/Chapter/Intro').search('p=123');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'});
+
+ log = '';
+ $location.path('/Blank').search('ignore');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current.params).toEqual({ignore:true});
+
+ log = '';
+ $location.path('/NONE');
+ $rootScope.$digest();
+ expect(log).toEqual('before();after();');
+ expect($route.current).toEqual(null);
+ });
+ });
+
+
+ it('should match a route that contains special chars in the path', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/$test.23/foo(bar)/:baz', {template: 'test.html'});
+ });
+ inject(function($route, $location, $rootScope) {
+
+ $location.path('/test');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+
+ $location.path('/$testX23/foo(bar)/222');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+
+ $location.path('/$test.23/foo(bar)/222');
+ $rootScope.$digest();
+ expect($route.current).toBeDefined();
+
+ $location.path('/$test.23/foo\\(bar)/222');
+ $rootScope.$digest();
+ expect($route.current).toBeUndefined();
+ });
+ });
+
+
+ it('should change route even when only search param changes', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/test', {template: 'test.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var callback = jasmine.createSpy('onRouteChange');
+
+ $rootScope.$on('$beforeRouteChange', callback);
+ $location.path('/test');
+ $rootScope.$digest();
+ callback.reset();
+
+ $location.search({any: true});
+ $rootScope.$digest();
+
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should allow routes to be defined with just templates without controllers', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {template: 'foo.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var onChangeSpy = jasmine.createSpy('onChange');
+
+ $rootScope.$on('$beforeRouteChange', onChangeSpy);
+ expect($route.current).toBeUndefined();
+ expect(onChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($route.current.template).toEqual('foo.html');
+ expect($route.current.controller).toBeUndefined();
+ expect(onChangeSpy).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should handle unknown routes with "otherwise" route definition', function() {
+ function NotFoundCtrl() {}
+
+ module(function($routeProvider){
+ $routeProvider.when('/foo', {template: 'foo.html'});
+ $routeProvider.otherwise({template: '404.html', controller: NotFoundCtrl});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var onChangeSpy = jasmine.createSpy('onChange');
+
+ $rootScope.$on('$beforeRouteChange', onChangeSpy);
+ expect($route.current).toBeUndefined();
+ expect(onChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/unknownRoute');
+ $rootScope.$digest();
+
+ expect($route.current.template).toBe('404.html');
+ expect($route.current.controller).toBe(NotFoundCtrl);
+ expect(onChangeSpy).toHaveBeenCalled();
+
+ onChangeSpy.reset();
+ $location.path('/foo');
+ $rootScope.$digest();
+
+ expect($route.current.template).toEqual('foo.html');
+ expect($route.current.controller).toBeUndefined();
+ expect(onChangeSpy).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should not fire $after/beforeRouteChange during bootstrap (if no route)', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/one', {}); // no otherwise defined
+ });
+
+ inject(function($rootScope, $route, $location) {
+ $rootScope.$on('$beforeRouteChange', routeChangeSpy);
+ $rootScope.$on('$afterRouteChange', routeChangeSpy);
+
+ $rootScope.$digest();
+ expect(routeChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/no-route-here');
+ $rootScope.$digest();
+ expect(routeChangeSpy).not.toHaveBeenCalled();
+ });
+ });
+
+
+ it('should match route with and without trailing slash', function() {
+ module(function($routeProvider){
+ $routeProvider.when('/foo', {template: 'foo.html'});
+ $routeProvider.when('/bar/', {template: 'bar.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.template).toBe('foo.html');
+
+ $location.path('/foo/');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.template).toBe('foo.html');
+
+ $location.path('/bar');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/');
+ expect($route.current.template).toBe('bar.html');
+
+ $location.path('/bar/');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar/');
+ expect($route.current.template).toBe('bar.html');
+ });
+ });
+
+
+ describe('redirection', function() {
+ it('should support redirection via redirectTo property by updating $location', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/', {redirectTo: '/foo'});
+ $routeProvider.when('/foo', {template: 'foo.html'});
+ $routeProvider.when('/bar', {template: 'bar.html'});
+ $routeProvider.when('/baz', {redirectTo: '/bar'});
+ $routeProvider.otherwise({template: '404.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ var onChangeSpy = jasmine.createSpy('onChange');
+
+ $rootScope.$on('$beforeRouteChange', onChangeSpy);
+ expect($route.current).toBeUndefined();
+ expect(onChangeSpy).not.toHaveBeenCalled();
+
+ $location.path('/');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/foo');
+ expect($route.current.template).toBe('foo.html');
+ expect(onChangeSpy.callCount).toBe(2);
+
+ onChangeSpy.reset();
+ $location.path('/baz');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/bar');
+ expect($route.current.template).toBe('bar.html');
+ expect(onChangeSpy.callCount).toBe(2);
+ });
+ });
+
+
+ it('should interpolate route vars in the redirected path from original path', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'});
+ $routeProvider.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo/id1/foo/subid3/gah');
+ $rootScope.$digest();
+
+ expect($location.path()).toEqual('/bar/id1/subid3/23');
+ expect($location.search()).toEqual({extraId: 'gah'});
+ expect($route.current.template).toEqual('bar.html');
+ });
+ });
+
+
+ it('should interpolate route vars in the redirected path from original search', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'});
+ $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo/id3/eId').search('subid=sid1&appended=true');
+ $rootScope.$digest();
+
+ expect($location.path()).toEqual('/bar/id3/sid1/99');
+ expect($location.search()).toEqual({appended: 'true', extra: 'eId'});
+ expect($route.current.template).toEqual('bar.html');
+ });
+ });
+
+
+ it('should allow custom redirectTo function to be used', function() {
+ function customRedirectFn(routePathParams, path, search) {
+ expect(routePathParams).toEqual({id: 'id3'});
+ expect(path).toEqual('/foo/id3');
+ expect(search).toEqual({ subid: 'sid1', appended: 'true' });
+ return '/custom';
+ }
+
+ module(function($routeProvider){
+ $routeProvider.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'});
+ $routeProvider.when('/foo/:id', {redirectTo: customRedirectFn});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $location.path('/foo/id3').search('subid=sid1&appended=true');
+ $rootScope.$digest();
+
+ expect($location.path()).toEqual('/custom');
+ });
+ });
+
+
+ it('should replace the url when redirecting', function() {
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:id', {template: 'bar.html'});
+ $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id'});
+ });
+ inject(function($route, $location, $rootScope) {
+ var replace;
+ $rootScope.$watch(function() {
+ if (isUndefined(replace)) replace = $location.$$replace;
+ });
+
+ $location.path('/foo/id3/eId');
+ $rootScope.$digest();
+
+ expect($location.path()).toEqual('/bar/id3');
+ expect(replace).toBe(true);
+ });
+ });
+ });
+
+
+ describe('reloadOnSearch', function() {
+ it('should reload a route when reloadOnSearch is enabled and .search() changes', function() {
+ var reloaded = jasmine.createSpy('route reload');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: noop});
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$on('$beforeRouteChange', reloaded);
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalled();
+ expect($routeParams).toEqual({});
+ reloaded.reset();
+
+ // trigger reload
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalled();
+ expect($routeParams).toEqual({foo:'bar'});
+ });
+ });
+
+
+ it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() {
+ var routeChange = jasmine.createSpy('route change'),
+ routeUpdate = jasmine.createSpy('route update');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$beforeRouteChange', routeChange);
+ $rootScope.$on('$afterRouteChange', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalled();
+ expect(routeChange.callCount).toBe(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ routeChange.reset();
+
+ // don't trigger reload
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalled();
+ });
+ });
+
+
+ it('should reload reloadOnSearch route when url differs only in route path param', function() {
+ var routeChange = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo/:fooId', {controller: noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$beforeRouteChange', routeChange);
+ $rootScope.$on('$afterRouteChange', routeChange);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ $location.path('/foo/aaa');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalled();
+ expect(routeChange.callCount).toBe(2);
+ routeChange.reset();
+
+ $location.path('/foo/bbb');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalled();
+ expect(routeChange.callCount).toBe(2);
+ routeChange.reset();
+
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ });
+ });
+
+
+ it('should update params when reloadOnSearch is disabled and .search() changes', function() {
+ var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: noop});
+ $routeProvider.when('/bar/:barId', {controller: noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$watch(function() {
+ return $routeParams;
+ }, function(value) {
+ routeParamsWatcher(value);
+ }, true);
+
+ expect(routeParamsWatcher).not.toHaveBeenCalled();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({});
+ routeParamsWatcher.reset();
+
+ // trigger reload
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({foo: 'bar'});
+ routeParamsWatcher.reset();
+
+ $location.path('/bar/123').search({});
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123'});
+ routeParamsWatcher.reset();
+
+ // don't trigger reload
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123', foo: 'bar'});
+ });
+ });
+
+
+ describe('reload', function() {
+
+ it('should reload even if reloadOnSearch is false', function() {
+ var routeChangeSpy = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/bar/:barId', {controller: noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$on('$afterRouteChange', routeChangeSpy);
+
+ $location.path('/bar/123');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId:'123'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ routeChangeSpy.reset();
+
+ $location.path('/bar/123').search('a=b');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId:'123', a:'b'});
+ expect(routeChangeSpy).not.toHaveBeenCalled();
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId:'123', a:'b'});
+ expect(routeChangeSpy).toHaveBeenCalledOnce();
+ });
+ });
+ });
+ });
+});
diff --git a/test/ng/sanitizeSpec.js b/test/ng/sanitizeSpec.js
new file mode 100644
index 00000000..a33d8992
--- /dev/null
+++ b/test/ng/sanitizeSpec.js
@@ -0,0 +1,281 @@
+'use strict';
+
+describe('HTML', function() {
+
+ var expectHTML;
+
+ beforeEach(inject(function($sanitize) {
+ expectHTML = function(html){
+ return expect($sanitize(html));
+ };
+ }));
+
+ describe('htmlParser', function() {
+ var handler, start, text;
+ beforeEach(function() {
+ handler = {
+ start: function(tag, attrs, unary){
+ start = {
+ tag: tag,
+ attrs: attrs,
+ unary: unary
+ };
+ // Since different browsers handle newlines differenttly we trim
+ // so that it is easier to write tests.
+ forEach(attrs, function(value, key){
+ attrs[key] = trim(value);
+ });
+ },
+ chars: function(text_){
+ text = text_;
+ },
+ end:function(tag) {
+ expect(tag).toEqual(start.tag);
+ }
+ };
+ });
+
+ it('should parse basic format', function() {
+ htmlParser('<tag attr="value">text</tag>', handler);
+ expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse newlines in tags', function() {
+ htmlParser('<\ntag\n attr="value"\n>text<\n/\ntag\n>', handler);
+ expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse newlines in attributes', function() {
+ htmlParser('<tag attr="\nvalue\n">text</tag>', handler);
+ expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse namespace', function() {
+ htmlParser('<ns:t-a-g ns:a-t-t-r="\nvalue\n">text</ns:t-a-g>', handler);
+ expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'value'}, unary:false});
+ expect(text).toEqual('text');
+ });
+
+ it('should parse empty value attribute of node', function() {
+ htmlParser('<OPTION selected value="">abc</OPTION>', handler);
+ expect(start).toEqual({tag:'option', attrs:{selected:'', value:''}, unary:false});
+ expect(text).toEqual('abc');
+ });
+
+ });
+
+ it('should echo html', function() {
+ expectHTML('hello<b class="1\'23" align=\'""\'>world</b>.').
+ toEqual('hello<b class="1\'23" align="&#34;&#34;">world</b>.');
+ });
+
+ it('should remove script', function() {
+ expectHTML('a<SCRIPT>evil< / scrIpt >c.').toEqual('ac.');
+ });
+
+ it('should remove nested script', function() {
+ expectHTML('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.').toEqual('ac.');
+ });
+
+ it('should remove attrs', function() {
+ expectHTML('a<div style="abc">b</div>c').toEqual('a<div>b</div>c');
+ });
+
+ it('should remove style', function() {
+ expectHTML('a<STyle>evil</stYle>c.').toEqual('ac.');
+ });
+
+ it('should remove script and style', function() {
+ expectHTML('a<STyle>evil<script></script></stYle>c.').toEqual('ac.');
+ });
+
+ it('should remove double nested script', function() {
+ expectHTML('a<SCRIPT>ev<script>evil</sCript>il</scrIpt>c.').toEqual('ac.');
+ });
+
+ it('should remove unknown names', function() {
+ expectHTML('a<xxx><B>b</B></xxx>c').toEqual('a<b>b</b>c');
+ });
+
+ it('should remove unsafe value', function() {
+ expectHTML('<a href="javascript:alert()">').toEqual('<a></a>');
+ });
+
+ it('should handle self closed elements', function() {
+ expectHTML('a<hr/>c').toEqual('a<hr/>c');
+ });
+
+ it('should handle namespace', function() {
+ expectHTML('a<my:hr/><my:div>b</my:div>c').toEqual('abc');
+ });
+
+ it('should handle entities', function() {
+ var everything = '<div rel="!@#$%^&amp;*()_+-={}[]:&#34;;\'&lt;&gt;?,./`~ &#295;">' +
+ '!@#$%^&amp;*()_+-={}[]:&#34;;\'&lt;&gt;?,./`~ &#295;</div>';
+ expectHTML(everything).toEqual(everything);
+ });
+
+ it('should handle improper html', function() {
+ expectHTML('< div rel="</div>" alt=abc dir=\'"\' >text< /div>').
+ toEqual('<div rel="&lt;/div&gt;" alt="abc" dir="&#34;">text</div>');
+ });
+
+ it('should handle improper html2', function() {
+ expectHTML('< div rel="</div>" / >').
+ toEqual('<div rel="&lt;/div&gt;"/>');
+ });
+
+ it('should ignore back slash as escape', function() {
+ expectHTML('<img alt="xxx\\" title="><script>....">').
+ toEqual('<img alt="xxx\\" title="&gt;&lt;script&gt;...."/>');
+ });
+
+ it('should ignore object attributes', function() {
+ expectHTML('<a constructor="hola">:)</a>').
+ toEqual('<a>:)</a>');
+ expectHTML('<constructor constructor="hola">:)</constructor>').
+ toEqual('');
+ });
+
+ describe('htmlSanitizerWriter', function() {
+ var writer, html;
+ beforeEach(function() {
+ html = '';
+ writer = htmlSanitizeWriter({push:function(text){html+=text;}});
+ });
+
+ it('should write basic HTML', function() {
+ writer.chars('before');
+ writer.start('div', {rel:'123'}, false);
+ writer.chars('in');
+ writer.end('div');
+ writer.chars('after');
+
+ expect(html).toEqual('before<div rel="123">in</div>after');
+ });
+
+ it('should escape text nodes', function() {
+ writer.chars('a<div>&</div>c');
+ expect(html).toEqual('a&lt;div&gt;&amp;&lt;/div&gt;c');
+ });
+
+ it('should escape IE script', function() {
+ writer.chars('&<>{}');
+ expect(html).toEqual('&amp;&lt;&gt;{}');
+ });
+
+ it('should escape attributes', function() {
+ writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'});
+ expect(html).toEqual('<div rel="!@#$%^&amp;*()_+-={}[]:&#34;;\'&lt;&gt;?,./`~ &#10;&#0;&#13;&#295;">');
+ });
+
+ it('should ignore missformed elements', function() {
+ writer.start('d>i&v', {});
+ expect(html).toEqual('');
+ });
+
+ it('should ignore unknown attributes', function() {
+ writer.start('div', {unknown:""});
+ expect(html).toEqual('<div>');
+ });
+
+ describe('explicitly dissallow', function() {
+ it('should not allow attributes', function() {
+ writer.start('div', {id:'a', name:'a', style:'a'});
+ expect(html).toEqual('<div>');
+ });
+
+ it('should not allow tags', function() {
+ function tag(name) {
+ writer.start(name, {});
+ writer.end(name);
+ }
+ tag('frameset');
+ tag('frame');
+ tag('form');
+ tag('param');
+ tag('object');
+ tag('embed');
+ tag('textarea');
+ tag('input');
+ tag('button');
+ tag('option');
+ tag('select');
+ tag('script');
+ tag('style');
+ tag('link');
+ tag('base');
+ tag('basefont');
+ expect(html).toEqual('');
+ });
+ });
+
+ describe('isUri', function() {
+
+ function isUri(value) {
+ return value.match(URI_REGEXP);
+ }
+
+ it('should be URI', function() {
+ expect(isUri('http://abc')).toBeTruthy();
+ expect(isUri('https://abc')).toBeTruthy();
+ expect(isUri('ftp://abc')).toBeTruthy();
+ expect(isUri('mailto:me@example.com')).toBeTruthy();
+ expect(isUri('#anchor')).toBeTruthy();
+ });
+
+ it('should not be UIR', function() {
+ expect(isUri('')).toBeFalsy();
+ expect(isUri('javascript:alert')).toBeFalsy();
+ });
+ });
+
+ describe('javascript URL attribute', function() {
+ beforeEach(function() {
+ this.addMatchers({
+ toBeValidUrl: function() {
+ return URI_REGEXP.exec(this.actual);
+ }
+ });
+ });
+
+ it('should ignore javascript:', function() {
+ expect('JavaScript:abc').not.toBeValidUrl();
+ expect(' \n Java\n Script:abc').not.toBeValidUrl();
+ expect('http://JavaScript/my.js').toBeValidUrl();
+ });
+
+ it('should ignore dec encoded javascript:', function() {
+ expect('&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;').not.toBeValidUrl();
+ expect('&#106&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;').not.toBeValidUrl();
+ expect('&#106 &#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;').not.toBeValidUrl();
+ });
+
+ it('should ignore decimal with leading 0 encodede javascript:', function() {
+ expect('&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058').not.toBeValidUrl();
+ expect('&#0000106 &#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058').not.toBeValidUrl();
+ expect('&#0000106; &#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058').not.toBeValidUrl();
+ });
+
+ it('should ignore hex encoded javascript:', function() {
+ expect('&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;').not.toBeValidUrl();
+ expect('&#x6A;&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;').not.toBeValidUrl();
+ expect('&#x6A &#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;').not.toBeValidUrl();
+ });
+
+ it('should ignore hex encoded whitespace javascript:', function() {
+ expect('jav&#x09;ascript:alert("A");').not.toBeValidUrl();
+ expect('jav&#x0A;ascript:alert("B");').not.toBeValidUrl();
+ expect('jav&#x0A ascript:alert("C");').not.toBeValidUrl();
+ expect('jav\u0000ascript:alert("D");').not.toBeValidUrl();
+ expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl();
+ expect(' &#14; java\u0000\u0000script:alert("D");').not.toBeValidUrl();
+ });
+ });
+
+ });
+
+});
diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js
new file mode 100644
index 00000000..b359e118
--- /dev/null
+++ b/test/ng/snifferSpec.js
@@ -0,0 +1,33 @@
+'use strict';
+
+describe('$sniffer', function() {
+
+ function sniffer($window) {
+ return new $SnifferProvider().$get[1]($window);
+ }
+
+ describe('history', function() {
+ it('should be true if history.pushState defined', function() {
+ expect(sniffer({history: {pushState: noop, replaceState: noop}}).history).toBe(true);
+ });
+
+ it('should be false if history or pushState not defined', function() {
+ expect(sniffer({history: {}}).history).toBe(false);
+ expect(sniffer({}).history).toBe(false);
+ });
+ });
+
+ describe('hashchange', function() {
+ it('should be true if onhashchange property defined', function() {
+ expect(sniffer({onhashchange: true, document: {}}).hashchange).toBe(true);
+ });
+
+ it('should be false if onhashchange property not defined', function() {
+ expect(sniffer({document: {}}).hashchange).toBe(false);
+ });
+
+ it('should be false if documentMode is 7 (IE8 comp mode)', function() {
+ expect(sniffer({onhashchange: true, document: {documentMode: 7}}).hashchange).toBe(false);
+ });
+ });
+});
diff --git a/test/ng/windowSpec.js b/test/ng/windowSpec.js
new file mode 100644
index 00000000..3b847146
--- /dev/null
+++ b/test/ng/windowSpec.js
@@ -0,0 +1,7 @@
+'use strict';
+
+describe('$window', function() {
+ it("should inject $window", inject(function($window) {
+ expect($window).toBe(window);
+ }));
+});