diff --git a/lib/angular.dart b/lib/angular.dart index 7f36d20d5..9d53acb3b 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -128,6 +128,9 @@ class AngularModule extends Module { type(Interpolate, Interpolate); type(CacheFactory, CacheFactory); type(Http, Http); + type(UrlRewriter, UrlRewriter); + type(HttpFutures, HttpFutures); + type(HttpBackend, HttpBackend); type(BlockCache, BlockCache); type(TemplateCache, TemplateCache); diff --git a/lib/block.dart b/lib/block.dart index b84d8b401..5e98a0938 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, BlockCache $blockCache) => - (new ComponentFactory(node, ref.directive))(injector, compiler, scope, parser, $blockCache), + nodeModule.factory(type, (Injector injector, Compiler compiler, Scope scope, Parser parser, BlockCache $blockCache, UrlRewriter urlRewriter) => + (new ComponentFactory(node, ref.directive))(injector, compiler, scope, parser, $blockCache, urlRewriter), visibility: visibility); } else { nodeModule.type(type, type, visibility: visibility); @@ -267,13 +267,13 @@ class ComponentFactory { ComponentFactory(this.element, this.directive); dynamic call(Injector injector, Compiler compiler, Scope scope, - Parser parser, BlockCache $blockCache) { + Parser parser, BlockCache $blockCache, UrlRewriter urlRewriter) { this.compiler = compiler; shadowDom = element.createShadowRoot(); shadowScope = scope.$new(true); createAttributeMapping(scope, shadowScope, parser); if (directive.$cssUrl != null) { - shadowDom.innerHtml = ''; + shadowDom.innerHtml = ''; } TemplateLoader templateLoader; if (directive.$template != null) { diff --git a/lib/http.dart b/lib/http.dart index 2cafbe5c2..8bd8ffe3c 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -1,18 +1,41 @@ part of angular; +// NOTE(deboer): This should be a generic utility class, but lets make sure +// it works in this case first! +class HttpFutures { + value(x) => new async.Future.value(x); +} + +class UrlRewriter { + String call(url) => url; +} + +class HttpBackend { + getString(String rawUrl, {bool withCredentials, void onProgress(dom.ProgressEvent e)}) { + return dom.HttpRequest.getString(url, withCredentials: withCredentials, onProgress: onProgress); + } +} + class Http { Map> _pendingRequests = >{}; - - async.Future getString(String url, {bool withCredentials, void onProgress(dom.ProgressEvent e), Cache cache}) { + UrlRewriter rewriter; + HttpBackend backend; + HttpFutures futures; + + Http(UrlRewriter this.rewriter, HttpBackend this.backend, HttpFutures this.futures); + + async.Future getString(String rawUrl, {bool withCredentials, void onProgress(dom.ProgressEvent e), Cache cache}) { + String url = rewriter(rawUrl); + // 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); + return futures.value(cachedValue); } - var result = dom.HttpRequest.getString(url, withCredentials: withCredentials, onProgress: onProgress).then((value) { + var result = backend.getString(url, withCredentials: withCredentials, onProgress: onProgress).then((value) { if (cache != null) { cache.put(url, value); } diff --git a/test/_http.dart b/test/_http.dart index 188cd9aa5..3e6e82140 100644 --- a/test/_http.dart +++ b/test/_http.dart @@ -9,6 +9,8 @@ class MockHttp extends Http { Map gets = {}; List futures = []; + MockHttp(UrlRewriter rewriter, HttpBackend backend, HttpFutures futures) : super(rewriter, backend, futures); + expectGET(String url, String content, {int times: 1}) { gets[url] = new MockHttpData(content, times); } @@ -46,4 +48,51 @@ class MockHttpData { toString() => value; } +class MockHttpFutures extends HttpFutures { + List completersAndValues = []; + Future value(x) { + var completer = new Completer.sync(); + completersAndValues.add([completer, x]); + return completer.future; + } + + trigger() { + completersAndValues.forEach((cv) => cv[0].complete(cv[1])); + completersAndValues = []; + } +} + +class MockHttpBackend extends HttpBackend { + Map gets = {}; + List completersAndValues = []; + + expectGET(String url, String content, {int times: 1}) { + gets[url] = new MockHttpData(content, times); + } + + flush() { + completersAndValues.forEach((cv) => cv[0].complete(cv[1])); + completersAndValues = []; + } + + assertAllGetsCalled() { + if (gets.length != 0) { + throw "Expected GETs not called $gets"; + } + } + + getString(String url, {bool withCredentials, void onProgress(ProgressEvent e)}) { + 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; + var completer = new Completer.sync(); + completersAndValues.add([completer, expectedValue]); + return completer.future; + } +} + main() {} diff --git a/test/http_spec.dart b/test/http_spec.dart new file mode 100644 index 000000000..e812e4b04 --- /dev/null +++ b/test/http_spec.dart @@ -0,0 +1,177 @@ +import "_specs.dart"; +import "_http.dart"; + +var VALUE = 'val'; +var CACHED_VALUE = 'cached_value'; + + +class FakeCache implements Cache { + get(x) => x == 'f' ? CACHED_VALUE : null; + put(_,__) => null; + +} + +class SubstringRewriter extends UrlRewriter { + call(String x) => x.substring(0, 1); +} + +main() { + describe('http rewriting', () { + var rewriter, futures, backend, cache; + beforeEach(() { + rewriter = new SubstringRewriter(); + futures = new MockHttpFutures(); + backend = new MockHttpBackend(); + cache = new FakeCache(); + }); + + it('should rewrite URLs before calling the backend', () { + backend.expectGET('a', VALUE, times: 1); + + var http = new Http(rewriter, backend, futures); + var called = 0; + http.getString('a[not sent to backed]').then((v) { + expect(v).toBe(VALUE); + called += 1; + }); + + expect(called).toEqual(0); + + backend.flush(); + + expect(called).toEqual(1); + backend.assertAllGetsCalled(); + }); + + it('should support pending requests for different raw URLs', () { + backend.expectGET('a', VALUE, times: 1); + + var http = new Http(rewriter, backend, futures); + var called = 0; + http.getString('a[some string]', cache: cache).then((v) { + expect(v).toBe(VALUE); + called += 1; + }); + http.getString('a[different string]', cache: cache).then((v) { + expect(v).toBe(VALUE); + called += 10; + }); + + expect(called).toEqual(0); + backend.flush(); + expect(called).toEqual(11); + backend.assertAllGetsCalled(); + }); + + it('should support caching', () { + var http = new Http(rewriter, backend, futures); + var called = 0; + http.getString('fromCache', cache: cache).then((v) { + expect(v).toBe(CACHED_VALUE); + called += 1; + }); + + expect(called).toEqual(0); + backend.flush(); + futures.trigger(); + + expect(called).toEqual(1); + backend.assertAllGetsCalled(); + }); + }); + + describe('http caching', () { + var rewriter, futures, backend, cache; + beforeEach(() { + rewriter = new UrlRewriter(); + futures = new MockHttpFutures(); + backend = new MockHttpBackend(); + cache = new FakeCache(); + }); + it('should not cache if no cache is present', () { + backend.expectGET('a', VALUE, times: 2); + + var http = new Http(rewriter, backend, futures); + var called = 0; + http.getString('a').then((v) { + expect(v).toBe(VALUE); + called += 1; + }); + http.getString('a').then((v) { + expect(v).toBe(VALUE); + called += 10; + }); + + expect(called).toEqual(0); + + backend.flush(); + + expect(called).toEqual(11); + backend.assertAllGetsCalled(); + }); + + + it('should return a pending request', inject(() { + backend.expectGET('a', VALUE, times: 1); + + var http = new Http(rewriter, backend, futures); + var called = 0; + http.getString('a', cache: cache).then((v) { + expect(v).toBe(VALUE); + called += 1; + }); + http.getString('a', cache: cache).then((v) { + expect(v).toBe(VALUE); + called += 10; + }); + + expect(called).toEqual(0); + backend.flush(); + expect(called).toEqual(11); + backend.assertAllGetsCalled(); + })); + + + it('should not return a pending request after the request is complete', () { + backend.expectGET('a', VALUE, times: 2); + + var http = new Http(rewriter, backend, futures); + var called = 0; + http.getString('a', cache: cache).then((v) { + expect(v).toBe(VALUE); + called += 1; + }); + + expect(called).toEqual(0); + backend.flush(); + + http.getString('a', cache: cache).then((v) { + expect(v).toBe(VALUE); + called += 10; + }); + + expect(called).toEqual(1); + backend.flush(); + expect(called).toEqual(11); + backend.assertAllGetsCalled(); + }); + + + it('should return a cached value if present', () { + var http = new Http(rewriter, backend, futures); + var called = 0; + // The URL string 'f' is primed in the FakeCache + http.getString('f', cache: cache).then((v) { + expect(v).toBe(CACHED_VALUE); + called += 1; + }); + + expect(called).toEqual(0); + backend.flush(); + futures.trigger(); + + expect(called).toEqual(1); + backend.assertAllGetsCalled(); + }); + }); +} diff --git a/test/templateurl_spec.dart b/test/templateurl_spec.dart index d67a410a9..4d8101e3a 100644 --- a/test/templateurl_spec.dart +++ b/test/templateurl_spec.dart @@ -32,7 +32,38 @@ class OnlyCssComponent { static String $cssUrl = 'simple.css'; } +class PrefixedUrlRewriter extends UrlRewriter { + call(url) => "PREFIX:$url"; +} + main() { + describe('loading with http rewriting', () { + var backend; + beforeEach(module((AngularModule module) { + backend = new MockHttpBackend(); + module + ..directive(HtmlAndCssComponent) + ..value(HttpBackend, backend) + ..type(UrlRewriter, PrefixedUrlRewriter); + })); + + it('should use the UrlRewriter for both HTML and CSS URLs', inject((Http $http, Compiler $compile, Scope $rootScope, Log log, Injector injector) { + + backend.expectGET('PREFIX:simple.html', '
Simple!
'); + + var element = $('
ignore
'); + $compile(element)(injector, element); + + backend.flush(); + + expect(renderedText(element)).toEqual('@import "PREFIX:simple.css"Simple!'); + expect(element[0].nodes[0].shadowRoot.innerHtml).toEqual( + '
Simple!
' + ); + })); + }); + + describe('async template loading', () { beforeEach(module((AngularModule module) { module.factory(Http, (Injector injector) => injector.get(MockHttp));