Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

feat(Http): UrlRewriter, HttpBackend for better tests. #38

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/angular.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 4 additions & 4 deletions lib/block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 = '<style>@import "${directive.$cssUrl}"</style>';
shadowDom.innerHtml = '<style>@import "${urlRewriter(directive.$cssUrl)}"</style>';
}
TemplateLoader templateLoader;
if (directive.$template != null) {
Expand Down
31 changes: 27 additions & 4 deletions lib/http.dart
Original file line number Diff line number Diff line change
@@ -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<String, async.Future<String>> _pendingRequests = <String, async.Future<String>>{};

async.Future<String> 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<String> 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);
}
Expand Down
49 changes: 49 additions & 0 deletions test/_http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class MockHttp extends Http {
Map<String, MockHttpData> 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);
}
Expand Down Expand Up @@ -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<String, MockHttpData> 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() {}
177 changes: 177 additions & 0 deletions test/http_spec.dart
Original file line number Diff line number Diff line change
@@ -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();
});
});
}
31 changes: 31 additions & 0 deletions test/templateurl_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', '<div log="SIMPLE">Simple!</div>');

var element = $('<div><html-and-css log>ignore</html-and-css><div>');
$compile(element)(injector, element);

backend.flush();

expect(renderedText(element)).toEqual('@import "PREFIX:simple.css"Simple!');
expect(element[0].nodes[0].shadowRoot.innerHtml).toEqual(
'<style>@import "PREFIX:simple.css"</style><div log="SIMPLE">Simple!</div>'
);
}));
});


describe('async template loading', () {
beforeEach(module((AngularModule module) {
module.factory(Http, (Injector injector) => injector.get(MockHttp));
Expand Down