diff --git a/lib/angular.dart b/lib/angular.dart index 0c3472eea..d84a07649 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -11,6 +11,7 @@ import 'debug.dart'; part 'block.dart'; part 'block_list.dart'; part 'block_type.dart'; +part 'cache.dart'; part 'compiler.dart'; part 'directive.dart'; part 'directives/ng_bind.dart'; @@ -124,7 +125,10 @@ class AngularModule extends Module { type(Scope, Scope); type(Parser, Parser); type(Interpolate, Interpolate); + type(CacheFactory, CacheFactory); type(Http, Http); + type(BlockCache, BlockCache); + type(TemplateCache, TemplateCache); value(ScopeDigestTTL, new ScopeDigestTTL(5)); diff --git a/lib/block.dart b/lib/block.dart index d5d6102cf..b84b64969 100644 --- a/lib/block.dart +++ b/lib/block.dart @@ -108,8 +108,8 @@ class Block implements ElementWrapper { if (ref.directive.isComponent) { //nodeModule.factory(type, new ComponentFactory(node, ref.directive), visibility: visibility); // TODO(misko): there should be no need to wrap function like this. - nodeModule.factory(type, (Injector injector, Compiler compiler, Scope scope, Parser parser, Http $http) => - (new ComponentFactory(node, ref.directive))(injector, compiler, scope, parser, $http), + nodeModule.factory(type, (Injector injector, Compiler compiler, Scope scope, Parser parser, BlockCache $blockCache) => + (new ComponentFactory(node, ref.directive))(injector, compiler, scope, parser, $blockCache), visibility: visibility); } else { nodeModule.type(type, type, visibility: visibility); @@ -266,36 +266,45 @@ class ComponentFactory { ComponentFactory(this.element, this.directive); - dynamic call(Injector injector, Compiler compiler, Scope scope, Parser parser, Http $http) { + dynamic call(Injector injector, Compiler compiler, Scope scope, + Parser parser, BlockCache $blockCache) { this.compiler = compiler; shadowDom = element.createShadowRoot(); shadowScope = scope.$new(true); createAttributeMapping(scope, shadowScope, parser); - var controller = createShadowInjector(injector).get(directive.type); if (directive.$cssUrl != null) { shadowDom.innerHtml = ''; } + TemplateLoader templateLoader; if (directive.$template != null) { - compileTemplate(directive.$template); + var blockFuture = new async.Future.value().then((_) => + attachBlockToShadowDom($blockCache.fromHtml(directive.$template))); + templateLoader = new TemplateLoader(blockFuture); } else if (directive.$templateUrl != null) { - $http. - getString(directive.$templateUrl). - then((data) => shadowScope.$apply(() => compileTemplate(data))); + var blockFuture = $blockCache.fromUrl(directive.$templateUrl) + .then((BlockType blockType) => attachBlockToShadowDom(blockType)); + templateLoader = new TemplateLoader(blockFuture); } + var controller = + createShadowInjector(injector, templateLoader).get(directive.type); if (directive.$publishAs != null) { shadowScope[directive.$publishAs] = controller; } return controller; } - compileTemplate(html) { - shadowDom.innerHtml += html; - compiler(shadowDom.nodes)(shadowInjector, shadowDom.nodes); + attachBlockToShadowDom(BlockType blockType) { + var block = blockType(shadowInjector); + shadowDom.nodes.addAll(block.elements); + shadowInjector.get(Scope).$digest(); + return shadowDom; } - createShadowInjector(injector) { - var shadowModule = new ScopeModule(shadowScope); - shadowModule.type(directive.type, directive.type); + createShadowInjector(injector, TemplateLoader templateLoader) { + var shadowModule = new ScopeModule(shadowScope) + ..type(directive.type, directive.type) + ..value(TemplateLoader, templateLoader) + ..value(dom.ShadowRoot, shadowDom); shadowInjector = injector.createChild([shadowModule]); // TODO(misko): creazy hack to mark injector shadowInjector.instances[_SHADOW] = injector; @@ -333,18 +342,70 @@ class ComponentFactory { } } +class BlockCache { + Cache _blockCache; + Http $http; + TemplateCache $templateCache; + Compiler compiler; + + BlockCache(CacheFactory $cacheFactory, Http this.$http, + TemplateCache this.$templateCache, Compiler this.compiler) { + _blockCache = $cacheFactory('blocks'); + } + + BlockType fromHtml(String html) { + BlockType blockType = _blockCache.get(html); + if (blockType == null) { + var div = new dom.Element.tag('div'); + div.innerHtml = html; + blockType = compiler(div.nodes); + _blockCache.put(html, blockType); + } + return blockType; + } + + async.Future fromUrl(String url) { + return $http.getString(url, cache: $templateCache).then((String tmpl) { + return fromHtml(tmpl); + }); + } +} + +/** + * A convinience wrapper for "templates" cache. + */ +class TemplateCache implements Cache { + Cache _cache; + + TemplateCache(CacheFactory $cacheFactory) { + _cache = $cacheFactory('templates'); + } + + Object get(key) => _cache.get(key); + Object put(key, Object value) => _cache.put(key, value); + void remove(key) => _cache.remove(key); + void removeAll() => _cache.removeAll(); + CacheInfo info() => _cache.info(); + void destroy() => _cache.destroy(); +} + +class TemplateLoader { + final async.Future _template; + async.Future get template => _template; + TemplateLoader(this._template); +} attrAccessorFactory(dom.Element element, String name) { return ([String value]) { if (value != null) { if (value == null) { - element.removeAttribute(name); + element.attributes.remove(name); } else { - element.setAttribute(name, value); + element.attributes[name] = value; } return value; } else { - return element.getAttribute(name); + return element.attributes[name]; } }; } diff --git a/lib/cache.dart b/lib/cache.dart new file mode 100644 index 000000000..d360e3be2 --- /dev/null +++ b/lib/cache.dart @@ -0,0 +1,98 @@ +part of angular; + +/** + * A simple map-backed cache. + * TODO(pavelgj): add LRU support. + */ +class Cache { + final String _id; + Map _data = {}; + CacheFactory _factory; + + Cache._newCache(String this._id, CacheFactory this._factory); + + Object get(key) { + _checkIfDestroyed(); + key = _stringifyKey(key); + return _data[key]; + } + + Object put(key, Object value) { + _checkIfDestroyed(); + if (value == null) { + return null; + } + key = _stringifyKey(key); + return _data[key] = value; + } + + void remove(key) { + _checkIfDestroyed(); + key = _stringifyKey(key); + _data.remove(key); + } + + void removeAll() { + _checkIfDestroyed(); + _data.clear(); + } + + void _checkIfDestroyed() { + if (_data == null) { + throw "[\$cacheFactory:iid] CacheId '$_id' is already destroyed!"; + } + } + + String _stringifyKey(key) { + if (!(key is String)) { + key = key.toString(); + } + return key; + } + + CacheInfo info() => new CacheInfo(size: _data.length, id: _id); + + void destroy() { + _data.clear(); + _data = null; + _factory._cacheMap.remove(_id); + } +} + +class CacheFactory { + Map _cacheMap = {}; + + Cache call(String cacheId) { + var cache = _cacheMap[cacheId]; + if (cache != null) { + throw "[\$cacheFactory:iid] CacheId '$cacheId' is already taken!"; + } + _cacheMap[cacheId] = cache = new Cache._newCache(cacheId, this); + return cache; + } + + Cache get(String cacheId) { + return _cacheMap[cacheId]; + } + + Map info() { + Map info = {}; + _cacheMap.keys.forEach((cacheId) { + info[cacheId] = _cacheMap[cacheId].info(); + }); + return info; + } +} + +class CacheInfo { + final int size; + final String id; + + CacheInfo({this.size, this.id}); + + bool operator ==(other) { + return other is CacheInfo && size == other.size && id == other.id; + } + + String toString() => '{size: $size, id: $id}'; +} \ No newline at end of file diff --git a/lib/http.dart b/lib/http.dart index 07569912c..2cafbe5c2 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -1,6 +1,28 @@ part of angular; class Http { - async.Future getString(String url, {bool withCredentials, void onProgress(dom.ProgressEvent e)}) => - dom.HttpRequest.getString(url, withCredentials: withCredentials, onProgress: onProgress); + Map> _pendingRequests = >{}; + + async.Future getString(String url, {bool withCredentials, void onProgress(dom.ProgressEvent e), Cache cache}) { + // We return a pending request only if caching is enabled. + if (cache != null && _pendingRequests.containsKey(url)) { + return _pendingRequests[url]; + } + var cachedValue = cache != null ? cache.get(url) : null; + if (cachedValue != null) { + return new async.Future.value(cachedValue); + } + var result = dom.HttpRequest.getString(url, withCredentials: withCredentials, onProgress: onProgress).then((value) { + if (cache != null) { + cache.put(url, value); + } + _pendingRequests.remove(url); + return value; + }, onError: (error) { + _pendingRequests.remove(url); + throw error; + }); + _pendingRequests[url] = result; + return result; + } } diff --git a/test/_http.dart b/test/_http.dart index 159884ba6..188cd9aa5 100644 --- a/test/_http.dart +++ b/test/_http.dart @@ -6,22 +6,44 @@ import 'dart:html'; import 'package:angular/angular.dart'; class MockHttp extends Http { - Map gets = {}; + Map gets = {}; List futures = []; - expectGET(url, content) { - gets[url] = content; + expectGET(String url, String content, {int times: 1}) { + gets[url] = new MockHttpData(content, times); } - flush() => gets.length == 0 ? Future.wait(futures) : + flush() => Future.wait(futures); + + assertAllGetsCalled() { + if (gets.length != 0) { throw "Expected GETs not called $gets"; + } + } - Future getString(String url, {bool withCredentials, void onProgress(ProgressEvent e)}) { - if (!gets.containsKey(url)) throw "Unexpected URL $url"; - var f = new Future.value(gets.remove(url)); - futures.add(f); - return f; + Future getString(String url, {bool withCredentials, void onProgress(ProgressEvent e), Cache cache}) { + if (!gets.containsKey(url)) throw "Unexpected URL $url $gets"; + var data = gets[url]; + data.times--; + if (data.times <= 0) { + gets.remove(url); + } + var expectedValue = data.value; + if (cache != null) { + cache.put(url, expectedValue); + } + var future = new Future.value(expectedValue); + futures.add(future); + return future; } } +class MockHttpData { + String value; + int times; + MockHttpData(this.value, this.times); + + toString() => value; +} + main() {} diff --git a/test/_http_spec.dart b/test/_http_spec.dart index e545bb9b2..62c4654b8 100644 --- a/test/_http_spec.dart +++ b/test/_http_spec.dart @@ -9,12 +9,43 @@ main() { }); it('should replay an http request', () { - http.expectGET('request', 'response'); - http.getString('request').then(expectAsync1((data) { - expect(data).toEqual('response'); - })); + http.expectGET('request', 'response'); + http.getString('request').then(expectAsync1((data) { + expect(data).toEqual('response'); + })); + }); + + it('should replay an http request which is expected multiple times', () { + http.expectGET('request', 'response', times: 2); + http.getString('request').then(expectAsync1((data) { + expect(http.gets.length).toEqual(1); + expect(data).toEqual('response'); + http.getString('request').then(expectAsync1((data) { + expect(http.gets.length).toEqual(0); + expect(data).toEqual('response'); + })); + })); + }); + + it('should throw an exeception on assertAllGetsCalled when not all expected GETs were called', () { + http.expectGET('request', 'response', times: 2); + http.getString('request').then(expectAsync1((data) { + expect(() { + http.assertAllGetsCalled(); + }).toThrow('Expected GETs not called {request: response}'); + })); }); + it('should cache results', inject((CacheFactory $cacheFactory) { + http.expectGET('request', 'response'); + Cache cache = $cacheFactory('test'); + http.getString('request', cache: cache).then(expectAsync1((data) { + expect(data).toEqual('response'); + expect(cache.info().size).toEqual(1); + expect(cache.get('request')).toEqual('response'); + })); + })); + it('should barf on an unseen request', () { expect(() { http.getString('unknown'); @@ -25,6 +56,7 @@ main() { http.expectGET('request', 'response'); expect(() { http.flush(); + http.assertAllGetsCalled(); }).toThrow('Expected GETs not called {request: response}'); }); }); diff --git a/test/_specs.dart b/test/_specs.dart index 58550de98..cf00e5175 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -85,6 +85,8 @@ class NotExpect { toThrow() => actual(); toHaveClass(cls) => unit.expect(actual.hasClass(cls), false, reason: ' Expected ${actual} to not have css class ${cls}'); + toBe(expected) => unit.expect(actual, + unit.predicate((actual) => !identical(expected, actual), '$expected')); } $(selector) { @@ -95,7 +97,7 @@ class JQuery implements List { List _list = []; JQuery([selector]) { - if (!?selector) { + if (selector == null) { // do nothing; } else if (selector is String) { _list.addAll(es(selector)); @@ -120,7 +122,7 @@ class JQuery implements List { accessor(Function getter, Function setter, [value]) { // TODO(dart): ?value does not work, since value was passed. :-( - var setterMode = ?value && value != null; + var setterMode = value != null; var result = setterMode ? this : ''; _list.forEach((node) { if (setterMode) { diff --git a/test/_test_bed.dart b/test/_test_bed.dart index 281b86161..bc6e0f5b8 100644 --- a/test/_test_bed.dart +++ b/test/_test_bed.dart @@ -58,3 +58,5 @@ class Probe { directive(Type type) => injector.get(type); } + +main() {} diff --git a/test/cache_spec.dart b/test/cache_spec.dart new file mode 100644 index 000000000..3cead14b5 --- /dev/null +++ b/test/cache_spec.dart @@ -0,0 +1,328 @@ +import "_specs.dart"; + +main() => describe('CacheFactory', () { + + it('should be injected', inject((CacheFactory $cacheFactory) { + expect($cacheFactory).toBeDefined(); + })); + + + it('should return a new cache whenever called', inject((CacheFactory $cacheFactory) { + var cache1 = $cacheFactory('cache1'); + var cache2 = $cacheFactory('cache2'); + expect(cache1).not.toBe(cache2); + })); + + + it('should complain if the cache id is being reused', inject((CacheFactory $cacheFactory) { + $cacheFactory('cache1'); + expect(() { $cacheFactory('cache1'); }). + toThrow("[\$cacheFactory:iid] CacheId 'cache1' is already taken!"); + })); + + + describe('info', () { + + it('should provide info about all created caches', inject((CacheFactory $cacheFactory) { + expect($cacheFactory.info()).toEqual({}); + + var cache1 = $cacheFactory('cache1'); + expect($cacheFactory.info()).toEqual({'cache1': new CacheInfo(id: 'cache1', size: 0)}); + + cache1.put('foo', 'bar'); + expect($cacheFactory.info()).toEqual({'cache1': new CacheInfo(id: 'cache1', size: 1)}); + })); + }); + + + describe('get', () { + + it('should return a cache if looked up by id', inject((CacheFactory $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', () { + var cache; + + beforeEach(inject((CacheFactory $cacheFactory) { + cache = $cacheFactory('test'); + })); + + + describe('put, get & remove', () { + + it('should add cache entries via add and retrieve them via get', inject((CacheFactory $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 null', inject((CacheFactory $cacheFactory) { + cache.put('key2', null); + + expect(cache.info().size).toBe(0); + })); + + + it('should remove entries via remove', inject((CacheFactory $cacheFactory) { + cache.put('k1', 'foo'); + cache.put('k2', 'bar'); + + cache.remove('k2'); + + expect(cache.get('k1')).toBe('foo'); + expect(cache.get('k2')).toBeNull(); + + cache.remove('k1'); + + expect(cache.get('k1')).toBeNull(); + expect(cache.get('k2')).toBeNull(); + })); + + + it('should return undefined when entry does not exist', inject((CacheFactory $cacheFactory) { + expect(cache.remove('non-existent')).toBeNull(); + })); + + + it('should stringify keys', inject((CacheFactory $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); + })); + + + it("should return value from put", inject((CacheFactory $cacheFactory) { + var obj = {}; + expect(cache.put('k1', obj)).toBe(obj); + })); + }); + + + describe('info', () { + + it('should size increment with put and decrement with remove', inject((CacheFactory $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((CacheFactory $cacheFactory) { + expect(cache.info().id).toBe('test'); + })); + }); + + + describe('removeAll', () { + + it('should blow away all data', inject((CacheFactory $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')).toBeNull(); + expect(cache.get('id2')).toBeNull(); + expect(cache.get('id3')).toBeNull(); + })); + }); + + + describe('destroy', () { + + it('should make the cache unusable and remove references to it from \$cacheFactory', inject((CacheFactory $cacheFactory) { + cache.put('foo', 'bar'); + cache.destroy(); + + expect(() { cache.get('foo'); } ).toThrow("[\$cacheFactory:iid] CacheId 'test' is already destroyed!"); + expect(() { cache.get('neverexisted'); }).toThrow("[\$cacheFactory:iid] CacheId 'test' is already destroyed!"); + expect(() { cache.put('foo', 'bar'); }).toThrow("[\$cacheFactory:iid] CacheId 'test' is already destroyed!"); + + expect($cacheFactory.get('test')).toBeNull(); + expect($cacheFactory.info()).toEqual({}); + })); + }); + }); + + + xdescribe('LRU cache', () { + + it('should create cache with defined capacity', inject((CacheFactory $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', () { + + beforeEach(inject((CacheFactory $cacheFactory) { + cache = $cacheFactory('cache1', {'capacity': 2}); + + cache.put('id0', 0); + cache.put('id1', 1); + })); + + + it('should kick out the first entry on put', inject((CacheFactory $cacheFactory) { + cache.put('id2', 2); + expect(cache.get('id0')).toBeNull(); + expect(cache.get('id1')).toBe(1); + expect(cache.get('id2')).toBe(2); + })); + + + it('should refresh an entry via get', inject((CacheFactory $cacheFactory) { + cache.get('id0'); + cache.put('id2', 2); + expect(cache.get('id0')).toBe(0); + expect(cache.get('id1')).toBeNull(); + expect(cache.get('id2')).toBe(2); + })); + + + it('should refresh an entry via put', inject((CacheFactory $cacheFactory) { + cache.put('id0', '00'); + cache.put('id2', 2); + expect(cache.get('id0')).toBe('00'); + expect(cache.get('id1')).toBeNull(); + expect(cache.get('id2')).toBe(2); + })); + + + it('should not purge an entry if another one was removed', inject((CacheFactory $cacheFactory) { + cache.remove('id1'); + cache.put('id2', 2); + expect(cache.get('id0')).toBe(0); + expect(cache.get('id1')).toBeNull(); + expect(cache.get('id2')).toBe(2); + })); + + + it('should purge the next entry if the stalest one was removed', inject((CacheFactory $cacheFactory) { + cache.remove('id0'); + cache.put('id2', 2); + cache.put('id3', 3); + expect(cache.get('id0')).toBeNull(); + expect(cache.get('id1')).toBeNull(); + 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((CacheFactory $cacheFactory) { + cache.remove('id0'); + cache.remove('id1'); + cache.put('id2', 2); + cache.put('id3', 3); + cache.put('id4', 4); + expect(cache.get('id0')).toBeNull(); + expect(cache.get('id1')).toBeNull(); + expect(cache.get('id2')).toBeNull(); + 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((CacheFactory $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')).toBeNull(); + expect(cache.get('id1')).toBeNull(); + expect(cache.get('id2')).toBeNull(); + expect(cache.get('id3')).toBe(3); + expect(cache.get('id4')).toBe(4); + })); + + + it('should correctly refresh and evict items if operations are chained', inject((CacheFactory $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')).toBeNull(); + expect(cache.get('id1')).toBeNull(); + expect(cache.get('id2')).toBeNull(); + expect(cache.get('id3')).toBeNull(); + expect(cache.get('id4')).toBe(4); + expect(cache.get('id5')).toBeNull(); + 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')).toBeNull(); + expect(cache.get('id1')).toBe(1); + expect(cache.get('id2')).toBe(2); + expect(cache.get('id3')).toBe(3); + })); + }); + }); +}); diff --git a/test/compiler_spec.dart b/test/compiler_spec.dart index f16a99fc1..586b5904c 100644 --- a/test/compiler_spec.dart +++ b/test/compiler_spec.dart @@ -403,7 +403,9 @@ main() { Block block = blockType(injector, element); $rootScope.$digest(); - expect(element.textWithShadow()).toEqual('OUTTER-_1:INNER_2(OUTTER-_1)'); + SimpleComponent.lastTemplateLoader.template.then(expectAsync1((_) { + expect(element.textWithShadow()).toEqual('OUTTER-_1:INNER_2(OUTTER-_1)'); + })); })); it('should create a component with IO', inject(() { @@ -434,7 +436,9 @@ main() { var element = $(r'
'); $compile(element)(injector, element); $rootScope.$apply(); - expect(element.textWithShadow()).toEqual('WORKED'); + PublishMeComponent.lastTemplateLoader.template.then(expectAsync1((_) { + expect(element.textWithShadow()).toEqual('WORKED'); + })); })); }); @@ -459,8 +463,10 @@ main() { class SimpleComponent { static String $template = r'{{name}}{{sep}}{{$id}}(SHADOW-CONTENT)'; - SimpleComponent(Scope scope) { + static TemplateLoader lastTemplateLoader; + SimpleComponent(Scope scope, TemplateLoader templateLoader) { scope.name = 'INNER'; + lastTemplateLoader = templateLoader; } } @@ -477,7 +483,10 @@ class IoComponent { class PublishMeComponent { static String $template = r'{{ctrlName.value}}'; static String $publishAs = 'ctrlName'; + static TemplateLoader lastTemplateLoader; String value = 'WORKED'; - PublishMeComponent() {} + PublishMeComponent(TemplateLoader templateLoader) { + lastTemplateLoader = templateLoader; + } } diff --git a/test/jasmine_syntax.dart b/test/jasmine_syntax.dart index a335a0695..603d6f4d9 100644 --- a/test/jasmine_syntax.dart +++ b/test/jasmine_syntax.dart @@ -72,13 +72,13 @@ class SpyFunction { SpyFunction([this.name]); call([a0, a1, a2, a3, a4, a5, a6]) { var args = []; - if (?a0) args.add(a0); - if (?a1) args.add(a1); - if (?a2) args.add(a2); - if (?a3) args.add(a3); - if (?a4) args.add(a4); - if (?a5) args.add(a5); - if (?a6) args.add(a6); + if (a0 != null) args.add(a0); + if (a1 != null) args.add(a1); + if (a2 != null) args.add(a2); + if (a3 != null) args.add(a3); + if (a4 != null) args.add(a4); + if (a5 != null) args.add(a5); + if (a6 != null) args.add(a6); invocations.add(args); } diff --git a/test/templateurl_spec.dart b/test/templateurl_spec.dart index ce3b84c82..f35c0807c 100644 --- a/test/templateurl_spec.dart +++ b/test/templateurl_spec.dart @@ -22,6 +22,10 @@ class HtmlAndCssComponent { class InlineWithCssComponent { static String $template = '
inline!
'; static String $cssUrl = 'simple.css'; + static TemplateLoader lastTemplateLoader; + InlineWithCssComponent(TemplateLoader templateLoader) { + lastTemplateLoader = templateLoader; + } } class OnlyCssComponent { @@ -40,6 +44,10 @@ main() { module.directive(OnlyCssComponent); module.directive(InlineWithCssComponent); })); + + afterEach(inject((MockHttp $http) { + $http.assertAllGetsCalled(); + })); it('should replace element with template from url', inject((MockHttp $http, Compiler $compile, Scope $rootScope, Log log, Injector injector) { $http.expectGET('simple.html', '
Simple!
'); @@ -54,6 +62,19 @@ main() { })); })); + it('should load template from URL once', inject((MockHttp $http, Compiler $compile, Scope $rootScope, Log log, Injector injector) { + $http.expectGET('simple.html', '
Simple!
', times: 2); + + var element = $('
ignoreignore
'); + $compile(element)(injector, element); + + $http.flush().then(expectAsync1((data) { + expect(renderedText(element)).toEqual('Simple!Simple!'); + // Note: There is no ordering. It is who ever comes off the wire first! + expect(log.result()).toEqual('LOG; LOG; SIMPLE; SIMPLE'); + })); + })); + it('should load a CSS file into a style', inject((MockHttp $http, Compiler $compile, Scope $rootScope, Log log, Injector injector) { $http.expectGET('simple.html', '
Simple!
'); @@ -73,7 +94,9 @@ main() { it('should load a CSS file with a \$template', inject((Compiler $compile, Scope $rootScope, Injector injector) { var element = $('
ignore
'); $compile(element)(injector, element); - expect(renderedText(element)).toEqual('@import "simple.css"inline!'); + InlineWithCssComponent.lastTemplateLoader.template.then(expectAsync1((_) { + expect(renderedText(element)).toEqual('@import "simple.css"inline!'); + })); })); it('should load a CSS with no template', inject((Compiler $compile, Scope $rootScope, Injector injector) {