diff options
Diffstat (limited to 'test/ng')
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<a>B</a>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>) ' + + '<<a href="http://a/">http://a/</a>> ' + + '<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="""">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="!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ">' + + '!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ</div>'; + expectHTML(everything).toEqual(everything); + }); + + it('should handle improper html', function() { + expectHTML('< div rel="</div>" alt=abc dir=\'"\' >text< /div>'). + toEqual('<div rel="</div>" alt="abc" dir=""">text</div>'); + }); + + it('should handle improper html2', function() { + expectHTML('< div rel="</div>" / >'). + toEqual('<div rel="</div>"/>'); + }); + + it('should ignore back slash as escape', function() { + expectHTML('<img alt="xxx\\" title="><script>....">'). + toEqual('<img alt="xxx\\" title="><script>...."/>'); + }); + + 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<div>&</div>c'); + }); + + it('should escape IE script', function() { + writer.chars('&<>{}'); + expect(html).toEqual('&<>{}'); + }); + + it('should escape attributes', function() { + writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'}); + expect(html).toEqual('<div rel="!@#$%^&*()_+-={}[]:";\'<>?,./`~ � ħ">'); + }); + + 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('javascript:').not.toBeValidUrl(); + expect('javascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + }); + + it('should ignore decimal with leading 0 encodede javascript:', function() { + expect('javascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + }); + + it('should ignore hex encoded javascript:', function() { + expect('javascript:').not.toBeValidUrl(); + expect('javascript:').not.toBeValidUrl(); + expect('j avascript:').not.toBeValidUrl(); + }); + + it('should ignore hex encoded whitespace javascript:', function() { + expect('jav	ascript:alert("A");').not.toBeValidUrl(); + expect('jav
ascript:alert("B");').not.toBeValidUrl(); + expect('jav
 ascript:alert("C");').not.toBeValidUrl(); + expect('jav\u0000ascript:alert("D");').not.toBeValidUrl(); + expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl(); + expect('  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); + })); +}); |
