diff --git a/angular_static.dart b/angular_static.dart new file mode 100644 index 000000000..e69de29bb diff --git a/example/pubspec.lock b/example/pubspec.lock index 08762dadd..6d13f3cd4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.11.10" + version: "0.12.2" angular: description: path: ".." @@ -15,17 +15,28 @@ packages: description: args source: hosted version: "0.9.0" + barback: + description: barback + source: hosted + version: "0.11.1" browser: description: browser source: hosted version: "0.9.1" + code_transformers: + description: code_transformers + source: hosted + version: "0.0.1-dev.2" collection: description: collection source: hosted version: "0.9.1" di: - description: di - source: hosted + description: + ref: null + resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" + url: "https://github.com/angular/di.dart.git" + source: git version: "0.0.33" html5lib: description: html5lib @@ -34,11 +45,15 @@ packages: intl: description: intl source: hosted - version: "0.9.7" + version: "0.8.10+4" logging: description: logging source: hosted version: "0.9.1+1" + meta: + description: meta + source: hosted + version: "0.8.8" path: description: path source: hosted diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0aff3ae1d..3e0e3b133 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,3 +5,16 @@ dependencies: path: ../ browser: any unittest: any + +transformers: +- angular: + dart_entries: + - web/todo.dart + - web/bouncing_balls.dart + html_files: # Need to split out to per-entry assets + - web/todo.html + - web/bouncing_balls.html + +dependency_overrides: + di: + git: https://github.com/angular/di.dart.git diff --git a/example/web/animation.dart b/example/web/animation.dart index 7ee1a7eea..210933dca 100644 --- a/example/web/animation.dart +++ b/example/web/animation.dart @@ -1,16 +1,9 @@ library animation; import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/animate/module.dart'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - part 'animation/repeat_demo.dart'; part 'animation/visibility_demo.dart'; part 'animation/stress_demo.dart'; @@ -25,11 +18,13 @@ class AnimationDemoController { } main() { - ngBootstrap(module: new Module() + new NgDynamicApp() + .addModule(new Module() ..install(new NgAnimateModule()) ..type(RepeatDemoComponent) ..type(VisibilityDemoComponent) ..type(StressDemoComponent) ..type(CssDemoComponent) - ..type(AnimationDemoController)); + ..type(AnimationDemoController)) + .run(); } diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index 783769740..c5bdd3437 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -1,4 +1,7 @@ +import 'package:perf_api/perf_api.dart'; import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; +import 'package:angular/change_detection/change_detection.dart'; import 'dart:html'; import 'dart:math'; import 'dart:core'; @@ -30,18 +33,17 @@ class BallModel { publishAs: 'bounce') class BounceController { var lastTime = window.performance.now(); - var run = true; + var run = false; var fps = 0; var digestTime = 0; var currentDigestTime = 0; var balls = []; - final NgZone zone; final Scope scope; var ballClassName = 'ball'; - BounceController(this.zone, this.scope) { + BounceController(this.scope) { changeCount(100); - tick(); + if (run) tick(); } void toggleCSS() { @@ -54,7 +56,7 @@ class BounceController { } void requestAnimationFrame(fn) { - window.requestAnimationFrame((_) => zone.run(fn)); + window.requestAnimationFrame((_) => fn()); } void changeCount(count) { @@ -118,20 +120,12 @@ class MyModule extends Module { MyModule() { type(BounceController); type(BallPositionDirective); - value(GetterCache, new GetterCache({ - 'x': (o) => o.x, - 'y': (o) => o.y, - 'bounce': (o) => o.bounce, - 'fps': (o) => o.fps, - 'balls': (o) => o.balls, - 'length': (o) => o.length, - 'digestTime': (o) => o.digestTime, - 'ballClassName': (o) => o.ballClassName - })); value(ScopeStats, new ScopeStats(report: true)); } } main() { - ngBootstrap(module: new MyModule()); + new NgDynamicApp() + .addModule(new MyModule()) + .run(); } diff --git a/example/web/hello_world.dart b/example/web/hello_world.dart index 338e33bb6..6b1f3957c 100644 --- a/example/web/hello_world.dart +++ b/example/web/hello_world.dart @@ -1,4 +1,5 @@ import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; // This annotation allows Dart to shake away any classes // not used from Dart code nor listed in another @MirrorsUsed. @@ -16,5 +17,7 @@ class HelloWorldController { } main() { - ngBootstrap(module: new Module()..type(HelloWorldController)); + new NgDynamicApp() + .addModule(new Module()..type(HelloWorldController)) + .run(); } diff --git a/example/web/todo.dart b/example/web/todo.dart index c480c9636..0da49f35f 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -1,19 +1,11 @@ library todo; import 'package:angular/angular.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/playback/playback_http.dart'; -import 'todo.dart'; import 'dart:html'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - class Item { String text; bool done; @@ -39,6 +31,7 @@ abstract class ServerController { // An implementation of ServerController that does nothing. +@NgInjectableService() class NoServerController implements ServerController { init(TodoController todo) { } } @@ -46,6 +39,7 @@ class NoServerController implements ServerController { // An implementation of ServerController that fetches items from // the server over HTTP. +@NgInjectableService() class HttpServerController implements ServerController { final Http _http; HttpServerController(this._http); @@ -128,5 +122,5 @@ main() { module.type(HttpBackend, implementedBy: PlaybackHttpBackend); } - ngBootstrap(module: module); + new NgDynamicApp().addModule(module).run(); } diff --git a/example/web/todo.html b/example/web/todo.html index aeeed8732..cbd32cc92 100644 --- a/example/web/todo.html +++ b/example/web/todo.html @@ -5,7 +5,7 @@ Things To Do - + diff --git a/lib/angular.dart b/lib/angular.dart index 136a01815..c190d6a7e 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -13,46 +13,8 @@ library angular; import 'dart:html' as dom; import 'dart:js' as js; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:intl/date_symbol_data_local.dart'; -/** - * If you are writing code accessed from Angular expressions, you must include - * your own @MirrorsUsed annotation or ensure that everything is tagged with - * the Ng annotations. - * - * All programs should also include a @MirrorsUsed(override: '*') which - * tells the compiler that only the explicitly listed libraries will - * be reflected over. - * - * This is a short-term fix until we implement a transformer-based solution - * which does not rely on mirrors. - */ -@MirrorsUsed(targets: const [ - 'angular', - 'angular.animate', - 'angular.core', - 'angular.core.dom', - 'angular.filter', - 'angular.perf', - 'angular.directive', - 'angular.routing', - 'angular.core.parser.Parser', - 'angular.core.parser.dynamic_parser', - 'angular.core.parser.lexer', - 'perf_api', - List, - dom.NodeTreeSanitizer, -], -metaTargets: const [ - NgInjectableService, - NgDirective, - NgController, - NgComponent, - NgFilter -]) -import 'dart:mirrors' show MirrorsUsed; - import 'package:angular/core/module.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/directive/module.dart'; @@ -68,8 +30,6 @@ export 'package:angular/core/parser/lexer.dart'; export 'package:angular/directive/module.dart'; export 'package:angular/filter/module.dart'; export 'package:angular/routing/module.dart'; -export 'package:angular/change_detection/dirty_checking_change_detector.dart' - show FieldGetter, GetterCache; part 'bootstrap.dart'; part 'introspection.dart'; diff --git a/lib/angular_dynamic.dart b/lib/angular_dynamic.dart new file mode 100644 index 000000000..41264e445 --- /dev/null +++ b/lib/angular_dynamic.dart @@ -0,0 +1,56 @@ +library angular.dynamic; + +import 'package:di/dynamic_injector.dart'; +import "package:angular/angular.dart"; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/core/registry_dynamic.dart'; +import 'dart:html' as dom; + +/** + * If you are writing code accessed from Angular expressions, you must include + * your own @MirrorsUsed annotation or ensure that everything is tagged with + * the Ng annotations. + * + * All programs should also include a @MirrorsUsed(override: '*') which + * tells the compiler that only the explicitly listed libraries will + * be reflected over. + * + * This is a short-term fix until we implement a transformer-based solution + * which does not rely on mirrors. + */ +@MirrorsUsed(targets: const [ + 'angular', + 'angular.animate', + 'angular.core', + 'angular.core.dom', + 'angular.filter', + 'angular.perf', + 'angular.directive', + 'angular.routing', + 'angular.core.parser.Parser', + 'angular.core.parser.dynamic_parser', + 'angular.core.parser.lexer', + 'perf_api', + List, + dom.NodeTreeSanitizer, +], +metaTargets: const [ + NgInjectableService, + NgDirective, + NgController, + NgComponent, + NgFilter +]) +import 'dart:mirrors' show MirrorsUsed; + +class NgDynamicApp extends NgApp { + NgDynamicApp() { + ngModule + ..type(MetadataExtractor, implementedBy: DynamicMetadataExtractor) + ..type(FieldGetterFactory, implementedBy: DynamicFieldGetterFactory); + } + + Injector createInjector() + => new DynamicInjector(modules: modules); +} diff --git a/lib/angular_static.dart b/lib/angular_static.dart new file mode 100644 index 000000000..d250f7d22 --- /dev/null +++ b/lib/angular_static.dart @@ -0,0 +1,28 @@ +library angular.static; + +// REMOVE once all mirrors dependencies are gone. +@MirrorsUsed(override: const ['*'], targets: const []) +import 'dart:mirrors'; + +import 'package:di/static_injector.dart'; +import 'package:angular/angular.dart'; +import 'package:angular/core/registry_static.dart'; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; + +class NgStaticApp extends NgApp { + final Map typeFactories; + + NgStaticApp(Map this.typeFactories, + Map metadata, + Map fieldGetters, + ClosureMap closureMap) { + ngModule + ..value(MetadataExtractor, new StaticMetadataExtractor(metadata)) + ..value(FieldGetterFactory, new StaticFieldGetterFactory(fieldGetters)) + ..value(ClosureMap, closureMap); + } + + Injector createInjector() + => new StaticInjector(modules: modules, typeFactories: typeFactories); +} diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 6c0172f88..d082691fc 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -23,13 +23,9 @@ class AngularModule extends Module { type(MetadataExtractor); value(Expando, _elementExpando); - value(NgApp, new NgApp(dom.window.document.documentElement)); } } -Injector _defaultInjectorFactory(List modules) => - new DynamicInjector(modules: modules); - /** * This method is the main entry point to an angular application. * @@ -62,46 +58,49 @@ Injector _defaultInjectorFactory(List modules) => * .... * Injector injector = ngBootstrap(module: myAppModule); */ -Injector ngBootstrap({ - Module module: null, - List modules: null, - dom.Element element: null, - String selector: '[ng-app]', - Injector injectorFactory(List modules): _defaultInjectorFactory}) { - _publishToJavaScript(); - var ngModules = [new AngularModule()]; - if (module != null) ngModules.add(module); - if (modules != null) ngModules.addAll(modules); - if (element == null) { - element = dom.querySelector(selector); - if (element == null) { - element = dom.window.document.childNodes - .firstWhere((e) => e is dom.Element); - } +abstract class NgApp { + static _find(String selector, [dom.Element defaultElement]) { + var element = dom.window.document.querySelector(selector); + if (element == null) element = defaultElement; + if (element == null)throw "Could not find application element '$selector'."; + return element; } - // The injector must be created inside the zone, so we create the - // zone manually and give it back to the injector as a value. - NgZone zone = new NgZone(); - ngModules.add(new Module() + final NgZone zone = new NgZone(); + final AngularModule ngModule = new AngularModule(); + final List modules = []; + dom.Element element; + + dom.Element selector(String selector) => element = _find(selector); + + NgApp(): element = _find('[ng-app]', dom.window.document.documentElement) { + modules.add(ngModule); + ngModule ..value(NgZone, zone) - ..value(NgApp, new NgApp(element))); + ..value(NgApp, this); + } + + Injector injector; - return zone.run(() { - var rootElements = [element]; - Injector injector = injectorFactory(ngModules); - initializeDateFormatting(null, null).then((_) { - var compiler = injector.get(Compiler); - var blockFactory = compiler(rootElements, injector.get(DirectiveMap)); - blockFactory(injector, rootElements); + NgApp addModule(Module module) { + modules.add(module); + return this; + } + + Injector run() { + _publishToJavaScript(); + return zone.run(() { + var rootElements = [element]; + Injector injector = createInjector(); + initializeDateFormatting(null, null).then((_) { + var compiler = injector.get(Compiler); + var blockFactory = compiler(rootElements, injector.get(DirectiveMap)); + blockFactory(injector, rootElements); + }); + return injector; }); - return injector; - }); -} + } -/// Holds a reference to the root of the application used by ngBootstrap. -class NgApp { - final dom.Element root; - NgApp(this.root); + Injector createInjector(); } diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index c3394708a..de368f258 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -230,6 +230,12 @@ abstract class RemovedItem extends CollectionChangeItem { RemovedItem get nextRemovedItem; } +typedef FieldGetter(object); + +abstract class FieldGetterFactory { + FieldGetter call(Object object, String name); +} + class AvgStopwatch extends Stopwatch { int _count = 0; diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 420ece3db..9852d2298 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -1,19 +1,8 @@ library dirty_checking_change_detector; -import 'dart:mirrors'; import 'dart:collection'; import 'package:angular/change_detection/change_detection.dart'; -typedef FieldGetter(object); - -class GetterCache { - final Map _map; - - GetterCache(this._map); - - FieldGetter call(String name) => _map[name]; -} - /** * [DirtyCheckingChangeDetector] determines which object properties have changed * by comparing them to the their previous value. @@ -47,7 +36,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { */ final DirtyCheckingRecord _marker = new DirtyCheckingRecord.marker(); - final GetterCache _getterCache; + final FieldGetterFactory _fieldGetterFactory; /** * All records for group are kept together and are denoted by head/tail. @@ -85,7 +74,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { */ DirtyCheckingChangeDetectorGroup _parent, _childHead, _childTail, _prev, _next; - DirtyCheckingChangeDetectorGroup(this._parent, this._getterCache) { + DirtyCheckingChangeDetectorGroup(this._parent, this._fieldGetterFactory) { // we need to insert the marker record at the beginning. if (_parent == null) { _recordHead = _marker; @@ -116,9 +105,8 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { WatchRecord watch(Object object, String field, H handler) { assert(_root != null); // prove that we are not deleted connected; - var getter = field == null ? null : _getterCache(field); - return _recordAdd(new DirtyCheckingRecord(this, object, field, getter, - handler)); + return _recordAdd(new DirtyCheckingRecord(this, _fieldGetterFactory, + handler, field, object)); } /** @@ -126,7 +114,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { */ DirtyCheckingChangeDetectorGroup newGroup() { assert(_root._assertRecordsOk()); - var child = new DirtyCheckingChangeDetectorGroup(this, _getterCache); + var child = new DirtyCheckingChangeDetectorGroup(this, _fieldGetterFactory); if (_childHead == null) { _childHead = _childTail = child; } else { @@ -250,7 +238,8 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup final DirtyCheckingRecord _fakeHead = new DirtyCheckingRecord.marker(); - DirtyCheckingChangeDetector(GetterCache getterCache): super(null, getterCache); + DirtyCheckingChangeDetector(FieldGetterFactory fieldGetterFactory) + : super(null, fieldGetterFactory); DirtyCheckingChangeDetector get _root => this; @@ -349,19 +338,17 @@ class _ChangeIterator implements Iterator>{ */ class DirtyCheckingRecord implements Record, WatchRecord { static const List _MODE_NAMES = - const ['MARKER', 'IDENT', 'REFLECT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP']; + const ['MARKER', 'IDENT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP']; static const int _MODE_MARKER_ = 0; static const int _MODE_IDENTITY_ = 1; - static const int _MODE_REFLECT_ = 2; - static const int _MODE_GETTER_ = 3; - static const int _MODE_MAP_FIELD_ = 4; - static const int _MODE_ITERABLE_ = 5; - static const int _MODE_MAP_ = 6; + static const int _MODE_GETTER_ = 2; + static const int _MODE_MAP_FIELD_ = 3; + static const int _MODE_ITERABLE_ = 4; + static const int _MODE_MAP_ = 5; final DirtyCheckingChangeDetectorGroup _group; + final FieldGetterFactory _fieldGetterFactory; final String field; - final Symbol _symbol; - final FieldGetter _getter; final H handler; int _mode; @@ -372,20 +359,18 @@ class DirtyCheckingRecord implements Record, WatchRecord { DirtyCheckingRecord _prevRecord; Record _nextChange; var _object; - InstanceMirror _instanceMirror; + FieldGetter _getter; - DirtyCheckingRecord(this._group, object, fieldName, this._getter, this.handler) - : field = fieldName, - _symbol = fieldName == null ? null : new Symbol(fieldName) - { + DirtyCheckingRecord(this._group, this._fieldGetterFactory, this.handler, + this.field, object) { this.object = object; } DirtyCheckingRecord.marker() - : handler = null, + : _group = null, + _fieldGetterFactory = null, + handler = null, field = null, - _group = null, - _symbol = null, _getter = null, _mode = _MODE_MARKER_; @@ -400,11 +385,12 @@ class DirtyCheckingRecord implements Record, WatchRecord { _object = obj; if (obj == null) { _mode = _MODE_IDENTITY_; + _getter = null; return; } if (field == null) { - _instanceMirror = null; + _getter = null; if (obj is Map) { if (_mode != _MODE_MAP_) { // Last one was collection as well, don't reset state. @@ -426,13 +412,10 @@ class DirtyCheckingRecord implements Record, WatchRecord { if (obj is Map) { _mode = _MODE_MAP_FIELD_; - _instanceMirror = null; - } else if (_getter != null) { - _mode = _MODE_GETTER_; - _instanceMirror = null; + _getter = null; } else { - _mode = _MODE_REFLECT_; - _instanceMirror = reflect(obj); + _mode = _MODE_GETTER_; + _getter = _fieldGetterFactory.call(obj, field); } } @@ -442,9 +425,6 @@ class DirtyCheckingRecord implements Record, WatchRecord { switch (_mode) { case _MODE_MARKER_: return false; - case _MODE_REFLECT_: - current = _instanceMirror.getField(_symbol).reflectee; - break; case _MODE_GETTER_: current = _getter(object); break; diff --git a/lib/change_detection/dirty_checking_change_detector_dynamic.dart b/lib/change_detection/dirty_checking_change_detector_dynamic.dart new file mode 100644 index 000000000..a34ac6cd2 --- /dev/null +++ b/lib/change_detection/dirty_checking_change_detector_dynamic.dart @@ -0,0 +1,19 @@ +library dirty_checking_change_detector_dynamic; + +import 'package:angular/change_detection/change_detection.dart'; + +/** + * We are using mirrors, but there is no need to import anything. + */ +@MirrorsUsed(targets: const [], metaTargets: const []) +import 'dart:mirrors'; + +class DynamicFieldGetterFactory implements FieldGetterFactory { + FieldGetter call (Object object, String name) { + Symbol symbol = new Symbol(name); + InstanceMirror instanceMirror = reflect(object); + return (Object object) { + return instanceMirror.getField(symbol).reflectee; + }; + } +} diff --git a/lib/change_detection/dirty_checking_change_detector_static.dart b/lib/change_detection/dirty_checking_change_detector_static.dart new file mode 100644 index 000000000..8267f71ea --- /dev/null +++ b/lib/change_detection/dirty_checking_change_detector_static.dart @@ -0,0 +1,15 @@ +library dirty_checking_change_detector_static; + +import 'package:angular/change_detection/change_detection.dart'; + +class StaticFieldGetterFactory implements FieldGetterFactory { + Map getters; + + StaticFieldGetterFactory(this.getters); + + FieldGetter call(Object object, String name) { + var getter = getters[name]; + if (getter == null) throw "Missing getter: (o) => o.$name"; + return getter; + } +} diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index b3085562a..1e4b49547 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -1,6 +1,5 @@ library angular.watch_group; -import 'dart:mirrors'; import 'package:angular/change_detection/change_detection.dart'; part 'linked_list.dart'; @@ -206,7 +205,8 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { _EvalWatchRecord _addEvalWatch(AST lhsAST, /* dartbug.com/16401 Function */ fn, String name, List argsAST, String expression) { _InvokeHandler invokeHandler = new _InvokeHandler(this, expression); - var evalWatchRecord = new _EvalWatchRecord(this, invokeHandler, fn, name, + var evalWatchRecord = new _EvalWatchRecord( + _rootGroup._fieldGetterFactory, this, invokeHandler, fn, name, argsAST.length); invokeHandler.watchRecord = evalWatchRecord; @@ -339,6 +339,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { * [RootWatchGroup] */ class RootWatchGroup extends WatchGroup { + final FieldGetterFactory _fieldGetterFactory; Watch _dirtyWatchHead, _dirtyWatchTail; /** @@ -351,7 +352,9 @@ class RootWatchGroup extends WatchGroup { int _removeCount = 0; - RootWatchGroup(ChangeDetector changeDetector, Object context): + RootWatchGroup(FieldGetterFactory this._fieldGetterFactory, + ChangeDetector changeDetector, + Object context): super._root(changeDetector, context); RootWatchGroup get _rootGroup => this; @@ -676,20 +679,19 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { WatchGroup watchGrp; final _Handler handler; final List args; - final Symbol symbol; final String name; int mode; /* dartbug.com/16401 Function*/ var fn; - InstanceMirror _instanceMirror; + FieldGetterFactory _fieldGetterFactory; bool dirtyArgs = true; dynamic currentValue, previousValue, _object; _EvalWatchRecord _prevEvalWatch, _nextEvalWatch; - _EvalWatchRecord(this.watchGrp, this.handler, this.fn, name, int arity) - : args = new List(arity), - name = name, - symbol = name == null ? null : new Symbol(name) { + _EvalWatchRecord(this._fieldGetterFactory, this.watchGrp, this.handler, + this.fn, this.name, int arity) + : args = new List(arity) + { if (fn is FunctionApply) { mode = _MODE_FUNCTION_APPLY_; } else if (fn is Function) { @@ -701,21 +703,21 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { _EvalWatchRecord.marker() : mode = _MODE_MARKER_, + _fieldGetterFactory = null, watchGrp = null, handler = null, args = null, fn = null, - symbol = null, name = null; _EvalWatchRecord.constant(_Handler handler, dynamic constantValue) : mode = _MODE_MARKER_, + _fieldGetterFactory = null, handler = handler, currentValue = constantValue, watchGrp = null, args = null, fn = null, - symbol = null, name = null; get field => '()'; @@ -727,7 +729,6 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { assert(mode != _MODE_MARKER_); assert(mode != _MODE_FUNCTION_); assert(mode != _MODE_FUNCTION_APPLY_); - assert(symbol != null); _object = value; if (value == null) { @@ -736,10 +737,21 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { if (value is Map) { mode = _MODE_MAP_CLOSURE_; } else { - _instanceMirror = reflect(value); - mode = _hasMethod(_instanceMirror, symbol) - ? _MODE_METHOD_ - : _MODE_FIELD_CLOSURE_; + var getter = _fieldGetterFactory.call(value, name); + // We need to know if we are referring to method or field which is a + // function We can find out by calling it twice and seeing if we get + // the same value. + var val1 = getter(_object); + var val2 = getter(_object); + if (identical(val1, val2)) { + // It is a field since calling it twice returns same value + fn = getter; + mode = _MODE_FIELD_CLOSURE_; + } else { + // It is a method since method closurizes into different instances + mode = _MODE_METHOD_; + fn = val1; + } } } } @@ -761,7 +773,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { dirtyArgs = false; break; case _MODE_FIELD_CLOSURE_: - var closure = _instanceMirror.getField(symbol).reflectee; + var closure = fn(_object); value = closure == null ? null : Function.apply(closure, args); break; case _MODE_MAP_CLOSURE_: @@ -769,7 +781,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { value = closure == null ? null : Function.apply(closure, args); break; case _MODE_METHOD_: - value = _instanceMirror.invoke(symbol, args).reflectee; + value = Function.apply(fn, args); break; default: assert(false); @@ -803,8 +815,4 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { if (mode == _MODE_MARKER_) return 'MARKER[$currentValue]'; return '${watchGrp.id}:${handler.expression}'; } - - static bool _hasMethod(InstanceMirror mirror, Symbol symbol) { - return mirror.type.instanceMembers[symbol] is MethodMirror; - } } diff --git a/lib/change_detection/watch_group_dynamic.dart b/lib/change_detection/watch_group_dynamic.dart new file mode 100644 index 000000000..78b8f4c52 --- /dev/null +++ b/lib/change_detection/watch_group_dynamic.dart @@ -0,0 +1,4 @@ +library watch_group_dynamic; + +import 'package:angular/change_detection/watch_group.dart'; + diff --git a/lib/change_detection/watch_group_static.dart b/lib/change_detection/watch_group_static.dart new file mode 100644 index 000000000..817d6a984 --- /dev/null +++ b/lib/change_detection/watch_group_static.dart @@ -0,0 +1,4 @@ +library watch_group_static; + +import 'package:angular/change_detection/watch_group.dart'; + diff --git a/lib/core/module.dart b/lib/core/module.dart index 4558a3d5f..846eec44e 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -2,7 +2,6 @@ library angular.core; import 'dart:async' as async; import 'dart:collection'; -import 'dart:mirrors'; import 'package:intl/intl.dart'; import 'package:di/di.dart'; @@ -43,7 +42,6 @@ class NgCoreModule extends Module { type(RootScope); factory(Scope, (injector) => injector.get(RootScope)); value(ScopeStats, new ScopeStats()); - value(GetterCache, new GetterCache({})); value(Object, {}); // RootScope context type(AstParser); type(NgZone); diff --git a/lib/core/parser/parser.dart b/lib/core/parser/parser.dart index 0e8af1a81..a205cfebb 100644 --- a/lib/core/parser/parser.dart +++ b/lib/core/parser/parser.dart @@ -4,8 +4,6 @@ export 'package:angular/core/parser/syntax.dart' show Visitor, Expression, BoundExpression; export 'package:angular/core/parser/dynamic_parser.dart' show DynamicParser, DynamicParserBackend, ClosureMap; -export 'package:angular/core/parser/static_parser.dart' - show StaticParser, StaticParserFunctions; typedef LocalsWrapper(context, locals); typedef Getter(self); diff --git a/lib/core/parser/static_parser.dart b/lib/core/parser/static_parser.dart deleted file mode 100644 index b9c59ee5d..000000000 --- a/lib/core/parser/static_parser.dart +++ /dev/null @@ -1,61 +0,0 @@ -library angular.core.parser.static_parser; - -import 'package:angular/core/module.dart' show FilterMap, NgInjectableService; -import 'package:angular/core/parser/parser.dart'; -import 'package:angular/core/parser/utils.dart' show EvalError; -import 'package:angular/core/parser/syntax.dart'; - -class StaticParserFunctions { - final Map eval; - final Map assign; - StaticParserFunctions(this.eval, this.assign); -} - -@NgInjectableService() -class StaticParser implements Parser { - final StaticParserFunctions _functions; - final DynamicParser _fallbackParser; - final Map _cache = {}; - StaticParser(this._functions, this._fallbackParser); - - Expression call(String input) { - if (input == null) input = ''; - return _cache.putIfAbsent(input, () => _construct(input)); - } - - Expression _construct(String input) { - var eval = _functions.eval[input]; - if (eval == null) return _fallbackParser(input); - if (eval is !Function) throw eval; - Function assign = _functions.assign[input]; - return new StaticExpression(input, eval, assign); - } -} - -class StaticExpression extends Expression { - final String _input; - final Function _eval; - final Function _assign; - StaticExpression(this._input, this._eval, [this._assign]); - - bool get isAssignable => _assign != null; - accept(Visitor visitor) => throw "Cannot visit static expression $this"; - toString() => _input; - - eval(scope, [FilterMap filters = defaultFilterMap]) { - try { - return _eval(scope, filters); - } on EvalError catch (e, s) { - throw e.unwrap("$this", s); - } - } - - assign(scope, value) { - try { - if (_assign == null) throw new EvalError("Cannot assign to $this"); - return _assign(scope, value); - } on EvalError catch (e, s) { - throw e.unwrap("$this", s); - } - } -} \ No newline at end of file diff --git a/lib/core/registry.dart b/lib/core/registry.dart index 11ed68651..8bfad16bf 100644 --- a/lib/core/registry.dart +++ b/lib/core/registry.dart @@ -70,12 +70,6 @@ abstract class AnnotationsMap { @NgInjectableService() -class MetadataExtractor { - Iterable call(Type type) { - if (reflectType(type) is TypedefMirror) return []; - var metadata = reflectClass(type).metadata; - return metadata == null - ? [] - : metadata.map((InstanceMirror im) => im.reflectee); - } +abstract class MetadataExtractor { + Iterable call(Type type); } diff --git a/lib/core/registry_dynamic.dart b/lib/core/registry_dynamic.dart new file mode 100644 index 000000000..49fff9537 --- /dev/null +++ b/lib/core/registry_dynamic.dart @@ -0,0 +1,77 @@ +library angular.core_dynamic; + +import 'dart:mirrors'; +import 'package:angular/core/module.dart'; + +class DynamicMetadataExtractor implements MetadataExtractor { + final _fieldAnnotations = [ + reflectType(NgAttr), + reflectType(NgOneWay), + reflectType(NgOneWayOneTime), + reflectType(NgTwoWay), + reflectType(NgCallback) + ]; + + Iterable call(Type type) { + if (reflectType(type) is TypedefMirror) return []; + var metadata = reflectClass(type).metadata; + if (metadata == null) { + metadata = []; + } else { + metadata = metadata.map((InstanceMirror im) => map(type, im.reflectee)); + } + return metadata; + } + + map(Type type, obj) { + if (obj is NgAnnotation) { + return mapDirectiveAnnotation(type, obj); + } else { + return obj; + } + } + + NgAnnotation mapDirectiveAnnotation(Type type, NgAnnotation annotation) { + var match; + var fieldMetadata = fieldMetadataExtractor(type); + if (fieldMetadata.isNotEmpty) { + var newMap = annotation.map == null ? {} : new Map.from(annotation.map); + fieldMetadata.forEach((String fieldName, AttrFieldAnnotation ann) { + var attrName = ann.attrName; + if (newMap.containsKey(attrName)) { + throw 'Mapping for attribute $attrName is already defined (while ' + 'processing annottation for field $fieldName of $type)'; + } + newMap[attrName] = '${ann.mappingSpec}$fieldName'; + }); + annotation = annotation.cloneWithNewMap(newMap); + } + return annotation; + } + + + Map fieldMetadataExtractor(Type type) { + ClassMirror cm = reflectType(type); + final fields = {}; + cm.declarations.forEach((Symbol name, DeclarationMirror decl) { + if (decl is VariableMirror || + decl is MethodMirror && (decl.isGetter || decl.isSetter)) { + var fieldName = MirrorSystem.getName(name); + if (decl is MethodMirror && decl.isSetter) { + // Remove "=" from the end of the setter. + fieldName = fieldName.substring(0, fieldName.length - 1); + } + decl.metadata.forEach((InstanceMirror meta) { + if (_fieldAnnotations.contains(meta.type)) { + if (fields.containsKey(fieldName)) { + throw 'Attribute annotation for $fieldName is defined more ' + 'than once in $type'; + } + fields[fieldName] = meta.reflectee as AttrFieldAnnotation; + } + }); + } + }); + return fields; + } +} diff --git a/lib/core/registry_static.dart b/lib/core/registry_static.dart new file mode 100644 index 000000000..78ad4e9fa --- /dev/null +++ b/lib/core/registry_static.dart @@ -0,0 +1,17 @@ +library angular.core_static; + +import 'package:angular/angular.dart'; +import 'package:angular/core/module.dart'; + +@NgInjectableService() +class StaticMetadataExtractor extends MetadataExtractor { + Map metadataMap; + final List empty = const []; + + StaticMetadataExtractor(this.metadataMap); + + Iterable call(Type type) { + Iterable i = metadataMap[type]; + return i == null ? empty : i; + } +} diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 707d59568..0246e6bca 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -389,7 +389,7 @@ class ScopeStats { } toString() => - 'digest #$_digestLoopNo:' + '${_digestLoopNo == 1 ? 'digest' : ' '} #$_digestLoopNo:' 'Field: ${_stat(digestFieldStopwatch)} ' 'Eval: ${_stat(digestEvalStopwatch)} ' 'Process: ${_stat(digestProcessStopwatch)}'; @@ -417,12 +417,14 @@ class RootScope extends Scope { String _state; RootScope(Object context, this._astParser, this._parser, - GetterCache cacheGetter, FilterMap filterMap, + FieldGetterFactory fieldGetterFactory, FilterMap filterMap, this._exceptionHandler, this._ttl, this._zone, this._scopeStats) : super(context, null, null, - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) + new RootWatchGroup(fieldGetterFactory, + new DirtyCheckingChangeDetector(fieldGetterFactory), context), + new RootWatchGroup(fieldGetterFactory, + new DirtyCheckingChangeDetector(fieldGetterFactory), context)) { _zone.onTurnDone = apply; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 6d5eb3e39..c1c04ac48 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -108,7 +108,7 @@ class NgZone { /** * A function called with any errors from the zone. */ - var onError = (e, s, ls) => null; + var onError = (e, s, ls) => print('$e\n$s\n$ls'); /** * A function that is called at the end of each VM turn in which the diff --git a/lib/core_dom/directive_map.dart b/lib/core_dom/directive_map.dart index affd2fab2..5a740af3a 100644 --- a/lib/core_dom/directive_map.dart +++ b/lib/core_dom/directive_map.dart @@ -9,62 +9,8 @@ class DirectiveMap extends AnnotationsMap { return _selector = _directiveSelectorFactory.selector(this); } - DirectiveMap(Injector injector, MetadataExtractor metadataExtractor, - FieldMetadataExtractor fieldMetadataExtractor, - this._directiveSelectorFactory) - : super(injector, metadataExtractor) { - final directives = >{}; - forEach((NgAnnotation annotation, Type type) { - var match; - var fieldMetadata = fieldMetadataExtractor(type); - if (fieldMetadata.isNotEmpty) { - var newMap = annotation.map == null ? {} : new Map.from(annotation.map); - fieldMetadata.forEach((String fieldName, AttrFieldAnnotation ann) { - var attrName = ann.attrName; - if (newMap.containsKey(attrName)) { - throw 'Mapping for attribute $attrName is already defined (while ' - 'processing annottation for field $fieldName of $type)'; - } - newMap[attrName] = '${ann.mappingSpec}$fieldName'; - }); - annotation = annotation.cloneWithNewMap(newMap); - } - directives.putIfAbsent(annotation, () => []).add(type); - }); - map - ..clear() - ..addAll(directives); - } -} - -@NgInjectableService() -class FieldMetadataExtractor implements Function { - final _fieldAnnotations = [reflectType(NgAttr), reflectType(NgOneWay), - reflectType(NgOneWayOneTime), reflectType(NgTwoWay), - reflectType(NgCallback)]; - - Map call(Type type) { - ClassMirror cm = reflectType(type); - final fields = {}; - cm.declarations.forEach((Symbol name, DeclarationMirror decl) { - if (decl is VariableMirror || - decl is MethodMirror && (decl.isGetter || decl.isSetter)) { - var fieldName = MirrorSystem.getName(name); - if (decl is MethodMirror && decl.isSetter) { - // Remove "=" from the end of the setter. - fieldName = fieldName.substring(0, fieldName.length - 1); - } - decl.metadata.forEach((InstanceMirror meta) { - if (_fieldAnnotations.contains(meta.type)) { - if (fields.containsKey(fieldName)) { - throw 'Attribute annotation for $fieldName is defined more ' - 'than once in $type'; - } - fields[fieldName] = meta.reflectee as AttrFieldAnnotation; - } - }); - } - }); - return fields; - } + DirectiveMap(Injector injector, + MetadataExtractor metadataExtractor, + this._directiveSelectorFactory) + : super(injector, metadataExtractor); } diff --git a/lib/core_dom/module.dart b/lib/core_dom/module.dart index 3fc9e2e89..2316df0f6 100644 --- a/lib/core_dom/module.dart +++ b/lib/core_dom/module.dart @@ -3,7 +3,6 @@ library angular.core.dom; import 'dart:async' as async; import 'dart:convert' show JSON; import 'dart:html' as dom; -import 'dart:mirrors'; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; @@ -50,7 +49,6 @@ class NgCoreDomModule extends Module { type(BrowserCookies); type(Cookies); type(LocationWrapper); - type(FieldMetadataExtractor); type(DirectiveMap); type(DirectiveSelectorFactory); } diff --git a/lib/metadata.dart b/lib/metadata.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mock/http_backend.dart b/lib/mock/http_backend.dart index 20a8e97a7..5108b8fb4 100644 --- a/lib/mock/http_backend.dart +++ b/lib/mock/http_backend.dart @@ -1,4 +1,12 @@ -part of angular.mock; +library angular.mock.http_backend; + +import 'dart:async' as dart_async; +import 'dart:convert' show JSON; +import 'dart:html'; + +import 'package:angular/angular.dart'; +import 'package:angular/utils.dart' as utils; + class _MockXhr { var $$method, $$url, $$async, $$reqHeaders, $$respHeaders; diff --git a/lib/mock/module.dart b/lib/mock/module.dart index 92b7386a6..a56a9acc2 100644 --- a/lib/mock/module.dart +++ b/lib/mock/module.dart @@ -2,25 +2,23 @@ library angular.mock; import 'dart:async' as dart_async; import 'dart:collection' show ListBase; -import 'dart:convert' show JSON; import 'dart:html'; import 'dart:js' as js; import 'package:angular/angular.dart'; -import 'package:angular/utils.dart' as utils; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:unittest/mock.dart'; +import 'http_backend.dart'; + +export 'http_backend.dart'; export 'zone.dart'; part 'debug.dart'; part 'exception_handler.dart'; -part 'http_backend.dart'; part 'log.dart'; part 'probe.dart'; part 'test_bed.dart'; part 'mock_window.dart'; -part 'test_injection.dart'; /** * Use in addition to [AngularModule] in your tests. diff --git a/lib/mock/test_injection.dart b/lib/mock/test_injection.dart index 2ca2b6c2c..0d14668b7 100644 --- a/lib/mock/test_injection.dart +++ b/lib/mock/test_injection.dart @@ -1,4 +1,9 @@ -part of angular.mock; +library angular.mock.test_injection; + +import 'package:angular/angular_dynamic.dart'; +import 'package:angular/mock/module.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; _SpecInjector _currentSpecInjector = null; @@ -127,7 +132,9 @@ module(fnOrModule) { void setUpInjector() { _currentSpecInjector = new _SpecInjector(); _currentSpecInjector.module((Module m) { - m..install(new AngularModule())..install(new AngularMockModule()); + m + ..install(new NgDynamicApp().ngModule) + ..install(new AngularMockModule()); }); } diff --git a/lib/playback/playback_http.dart b/lib/playback/playback_http.dart index f6253a3d9..8b3d6afaa 100644 --- a/lib/playback/playback_http.dart +++ b/lib/playback/playback_http.dart @@ -6,7 +6,7 @@ import 'dart:html'; import 'package:angular/core_dom/module.dart'; import 'package:angular/core/service.dart'; -import 'package:angular/mock/module.dart' as mock; +import 'package:angular/mock/http_backend.dart' as mock; import 'package:angular/playback/playback_data.dart' as playback_data; diff --git a/lib/routing/routing.dart b/lib/routing/routing.dart index 4a26bf6f8..948c3f26a 100644 --- a/lib/routing/routing.dart +++ b/lib/routing/routing.dart @@ -137,7 +137,7 @@ class NgRoutingHelper { }); }); - router.listen(appRoot: _ngApp.root); + router.listen(appRoot: _ngApp.element); } _reloadViews({Route startingFrom}) { diff --git a/lib/tools/parser_getter_setter/generator.dart b/lib/tools/parser_getter_setter/generator.dart index b33244e10..b58c241c3 100644 --- a/lib/tools/parser_getter_setter/generator.dart +++ b/lib/tools/parser_getter_setter/generator.dart @@ -58,7 +58,7 @@ class StaticClosureMap extends ClosureMap { => _getters[name]; Setter lookupSetter(String name) => _setters[name]; - lookupFunction(String name, int arity) + lookupFunction(String name, int arity) => (arity < _functions.length) ? _functions[arity][name] : null; } '''; @@ -84,7 +84,7 @@ class StaticClosureMap extends ClosureMap { var maxArity = arities.keys.reduce((x, y) => max(x, y)); - var maps = new Iterable.generate(maxArity, (arity) { + var maps = new Iterable.generate(maxArity + 1, (arity) { var names = arities[arity]; if (names == null) { return '{\n }'; diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart new file mode 100644 index 000000000..2b3a9b284 --- /dev/null +++ b/lib/tools/transformer/expression_generator.dart @@ -0,0 +1,188 @@ +library angular.tools.transformer.expression_generator; + +import 'dart:async'; +import 'dart:math' as math; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/tools/html_extractor.dart'; +import 'package:angular/tools/parser_getter_setter/generator.dart'; +import 'package:angular/tools/source_crawler.dart'; +import 'package:angular/tools/source_metadata_extractor.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; +import 'package:path/path.dart' as path; + +/** + * Transformer which gathers all expressions from the HTML source files and + * Dart source files of an application and packages them for static evaluation. + * + * This will also modify the main Dart source file to import the generated + * expressions and modify all references to NG_EXPRESSION_MODULE to refer to + * the generated expressions. + */ +class ExpressionGenerator extends Transformer with ResolverTransformer { + final TransformOptions options; + + ExpressionGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => options.isDartEntry(input.id); + + Future applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + var outputBuffer = new StringBuffer(); + + _writeStaticExpressionHeader(asset.id, outputBuffer); + + var sourceMetadataExtractor = new SourceMetadataExtractor(); + var directives = + sourceMetadataExtractor.gatherDirectiveInfo(null, + new _LibrarySourceCrawler(resolver.libraries)); + + var htmlExtractor = new HtmlExpressionExtractor(directives); + return _getHtmlSources(transform) + .forEach(htmlExtractor.parseHtml) + .then((_) { + var module = new Module() + ..type(Parser, implementedBy: DynamicParser) + ..type(ParserBackend, implementedBy: DartGetterSetterGen); + var injector = + new DynamicInjector(modules: [module], allowImplicitInjection: true); + + injector.get(_ParserGetterSetter).generateParser( + htmlExtractor.expressions.toList(), outputBuffer); + + var id = transform.primaryInput.id; + var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' + '_static_expressions.dart'; + var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); + var outputId = new AssetId(id.package, outputPath); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + + transform.addOutput(asset); + }); + } + + /** + * Gets a stream consisting of the contents of all HTML source files to be + * scoured for expressions. + */ + Stream _getHtmlSources(Transform transform) { + var controller = new StreamController(); + if (options.htmlFiles == null) { + controller.close(); + return controller.stream; + } + Future.wait(options.htmlFiles.map((path) { + var htmlId = new AssetId(transform.primaryInput.id.package, path); + return transform.readInputAsString(htmlId); + }).map((future) { + return future.then(controller.add).catchError(controller.addError); + })).then((_) { + controller.close(); + }); + return controller.stream; + } +} + +void _writeStaticExpressionHeader(AssetId id, StringSink sink) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_expressions; + +import 'package:angular/angular.dart'; +import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; + +Module get expressionModule => new Module() + ..value(ClosureMap, new StaticClosureMap()); + +'''); +} + +class _LibrarySourceCrawler implements SourceCrawler { + final List libraries; + _LibrarySourceCrawler(this.libraries); + + void crawl(String entryPoint, CompilationUnitVisitor visitor) { + libraries.expand((lib) => lib.units) + .map((compilationUnitElement) => compilationUnitElement.node) + .forEach(visitor); + } +} + +class _ParserGetterSetter { + final Parser parser; + final ParserBackend backend; + _ParserGetterSetter(this.parser, this.backend); + + generateParser(List exprs, StringSink sink) { + exprs.forEach((expr) { + try { + parser(expr); + } catch (e) { + // Ignore exceptions. + } + }); + + DartGetterSetterGen backend = this.backend; + sink.write(generateClosureMap(backend.properties, backend.calls)); + } + + String generateClosureMap(Set properties, + Map> calls) { + return ''' +class StaticClosureMap extends ClosureMap { + Getter lookupGetter(String name) => getters[name]; + Setter lookupSetter(String name) => setters[name]; + lookupFunction(String name, int arity) + => (arity < functions.length) ? functions[arity][name] : null; +} + +final Map getters = ${generateGetterMap(properties)}; +final Map setters = ${generateSetterMap(properties)}; +final List> functions = ${generateFunctionMap(calls)}; +'''; + } + + generateGetterMap(Iterable keys) { + var lines = keys.map((key) => 'r"${key}": (o) => o.$key'); + return '{\n ${lines.join(",\n ")}\n}'; + } + + generateSetterMap(Iterable keys) { + var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); + return '{\n ${lines.join(",\n ")}\n}'; + } + + generateFunctionMap(Map> calls) { + Map> arities = {}; + calls.forEach((name, callArities) { + callArities.forEach((arity){ + arities.putIfAbsent(arity, () => new Set()).add(name); + }); + }); + + var maxArity = arities.isEmpty ? 0 : + arities.keys.reduce((x, y) => math.max(x, y)); + + var maps = new Iterable.generate(maxArity + 1, (arity) { + var names = arities[arity]; + if (names == null) { + return '{\n }'; + } else { + var args = new List.generate(arity, (e) => "a$e").join(','); + var p = args.isEmpty ? '' : ', $args'; + var lines = names.map((name) => 'r"$name": (o$p) => o.$name($args)'); + return '{\n ${lines.join(",\n ")}\n }'; + } + }); + + return '[${maps.join(",")}]'; + } +} + diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart new file mode 100644 index 000000000..83cd56dd3 --- /dev/null +++ b/lib/tools/transformer/metadata_extractor.dart @@ -0,0 +1,425 @@ +library angular.metadata_extractor; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:analyzer/src/generated/scanner.dart'; +import 'package:analyzer/src/generated/utilities_dart.dart' show ParameterKind; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; + +class AnnotatedType { + final ClassElement type; + Iterable annotations; + + AnnotatedType(this.type); + + /** + * Finds all the libraries referenced by the annotations + */ + Iterable get referencedLibraries { + var libs = new Set(); + libs.add(type.library); + + var libCollector = new _LibraryCollector(); + for (var annotation in annotations) { + annotation.accept(libCollector); + } + libs.addAll(libCollector.libraries); + + return libs; + } + + void writeClassAnnotations(StringBuffer sink, TransformLogger logger, + Resolver resolver, Map prefixes) { + sink.write(' ${prefixes[type.library]}${type.name}: const [\n'); + var writer = new _AnnotationWriter(sink, prefixes); + for (var annotation in annotations) { + sink.write(' '); + if (writer.writeAnnotation(annotation)) { + sink.write(',\n'); + } else { + sink.write('null,\n'); + logger.warning('Unable to serialize annotation $annotation.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + } + } + sink.write(' ],\n'); + } +} + +/** + * Helper which finds all libraries referenced within the provided AST. + */ +class _LibraryCollector extends GeneralizingASTVisitor { + final Set libraries = new Set(); + void visitSimpleIdentifier(SimpleIdentifier s) { + var element = s.bestElement; + if (element != null) { + libraries.add(element.library); + } + } +} + +/** + * Helper class which writes annotations out to the buffer. + * This does not support every syntax possible, but will return false when + * the annotation cannot be serialized. + */ +class _AnnotationWriter { + final StringBuffer sink; + final Map prefixes; + + _AnnotationWriter(this.sink, this.prefixes); + + /** + * Returns true if the annotation was successfully serialized. + * If the annotation could not be written then the buffer is returned to its + * original state. + */ + bool writeAnnotation(Annotation annotation) { + // Record the current location in the buffer and if writing fails then + // back up the buffer to where we started. + var len = sink.length; + if (!_writeAnnotation(annotation)) { + var str = sink.toString(); + sink.clear(); + sink.write(str.substring(0, len)); + return false; + } + return true; + } + + bool _writeAnnotation(Annotation annotation) { + var element = annotation.element; + if (element is ConstructorElement) { + sink.write('const ${prefixes[element.library]}' + '${element.enclosingElement.name}'); + // Named constructors + if (!element.name.isEmpty) { + sink.write('.${element.name}'); + } + sink.write('('); + if (!_writeArguments(annotation)) return false; + sink.write(')'); + return true; + } else if (element is PropertyAccessorElement) { + sink.write('${prefixes[element.library]}${element.name}'); + return true; + } + + return false; + } + + /** Writes the arguments for a type constructor. */ + bool _writeArguments(Annotation annotation) { + var args = annotation.arguments; + var index = 0; + for (var arg in args.arguments) { + if (arg is NamedExpression) { + sink.write('${arg.name.label.name}: '); + if (!_writeExpression(arg.expression)) return false; + } else { + if (!_writeExpression(arg)) return false; + } + if (++index < args.arguments.length) { + sink.write(', '); + } + } + return true; + } + + /** Writes an expression. */ + bool _writeExpression(Expression expression) { + if (expression is StringLiteral) { + var str = expression.stringValue + .replaceAll(r'\', r'\\') + .replaceAll('\'', '\\\''); + sink.write('\'$str\''); + return true; + } + if (expression is ListLiteral) { + sink.write('const ['); + for (var element in expression.elements) { + if (!_writeExpression(element)) return false; + sink.write(','); + } + sink.write(']'); + return true; + } + if (expression is MapLiteral) { + sink.write('const {'); + var index = 0; + for (var entry in expression.entries) { + if (!_writeExpression(entry.key)) return false; + sink.write(': '); + if (!_writeExpression(entry.value)) return false; + if (++index < expression.entries.length) { + sink.write(', '); + } + } + sink.write('}'); + return true; + } + if (expression is Identifier) { + var element = expression.bestElement; + if (element == null || !element.isPublic) return false; + + if (element is ClassElement) { + sink.write('${prefixes[element.library]}${element.name}'); + return true; + } + if (element is PropertyAccessorElement) { + var variable = element.variable; + if (variable is FieldElement) { + var cls = variable.enclosingElement; + sink.write('${prefixes[cls.library]}${cls.name}.${variable.name}'); + return true; + } else if (variable is TopLevelVariableElement) { + sink.write('${prefixes[variable.library]}${variable.name}'); + return true; + } + print('variable ${variable.runtimeType} $variable'); + } + print('element ${element.runtimeType} $element'); + } + if (expression is BooleanLiteral) { + sink.write(expression.value); + return true; + } + if (expression is DoubleLiteral) { + sink.write(expression.value); + return true; + } + if (expression is IntegerLiteral) { + sink.write(expression.value); + return true; + } + if (expression is NullLiteral) { + sink.write('null'); + return true; + } + print('expression ${expression.runtimeType} $expression'); + return false; + } +} + +class AnnotationExtractor { + final TransformLogger logger; + final Resolver resolver; + final AssetId outputId; + + static const List _angularAnnotationNames = const [ + 'angular.core.NgAttr', + 'angular.core.NgOneWay', + 'angular.core.NgOneWayOneTime', + 'angular.core.NgTwoWay', + 'angular.core.NgCallback' + ]; + + static const Map _annotationToMapping = const { + 'NgAttr': '@', + 'NgOneWay': '=>', + 'NgOneWayOneTime': '=>!', + 'NgTwoWay': '<=>', + 'NgCallback': '&', + }; + + ClassElement ngAnnotationType; + + /// Resolved annotations that this will pick up for members. + final List _annotationElements = []; + + AnnotationExtractor(this.logger, this.resolver, this.outputId) { + for (var annotation in _angularAnnotationNames) { + var type = resolver.getType(annotation); + if (type == null) { + logger.warning('Unable to resolve $annotation, skipping metadata.'); + continue; + } + _annotationElements.add(type.unnamedConstructor); + } + ngAnnotationType = resolver.getType('angular.core.NgAnnotation'); + if (ngAnnotationType == null) { + logger.warning('Unable to resolve NgAnnotation, ' + 'skipping member annotations.'); + } + } + + AnnotatedType extractAnnotations(ClassElement cls) { + if (resolver.getImportUri(cls.library, from: outputId) == null) { + warn('Dropping annotations for ${cls.name} because the ' + 'containing file cannot be imported (must be in a lib folder).', cls); + return null; + } + + var visitor = new _AnnotationVisitor(_annotationElements); + cls.node.accept(visitor); + + if (!visitor.hasAnnotations) return null; + + var type = new AnnotatedType(cls); + type.annotations = visitor.classAnnotations + .where((annotation) { + var element = annotation.element; + if (element != null && !element.isPublic) { + warn('Annotation $annotation is not public.', + annotation.parent.element); + return false; + } + if (element is ConstructorElement && + !element.enclosingElement.isPublic) { + warn('Annotation $annotation is not public.', + annotation.parent.element); + return false; + } + return true; + }).toList(); + + + var memberAnnotations = {}; + visitor.memberAnnotations.forEach((memberName, annotations) { + if (annotations.length > 1) { + warn('$memberName can only have one annotation.', + annotations[0].parent.element); + return; + } + + memberAnnotations[memberName] = annotations[0]; + }); + + if (memberAnnotations.isNotEmpty) { + _foldMemberAnnotations(memberAnnotations, type); + } + + if (type.annotations.isEmpty) return null; + + return type; + } + + /// Folds all AttrFieldAnnotations into the NgAnnotation annotation on the + /// class. + _foldMemberAnnotations(Map memberAnnotations, + AnnotatedType type) { + // Filter down to NgAnnotation constructors. + var ngAnnotations = type.annotations.where((a) { + var element = a.element; + if (element is! ConstructorElement) return false; + return element.enclosingElement.type.isAssignableTo( + ngAnnotationType.type); + }); + if (ngAnnotations.isEmpty) { + warn('Found field annotation but no class directives.', type.type); + return; + } + + var mapType = resolver.getType('dart.core.Map').type; + // Find acceptable constructors- ones which take a param named 'map' + var acceptableAnnotations = ngAnnotations.where((a) { + var ctor = a.element; + + for (var param in ctor.parameters) { + if (param.parameterKind != ParameterKind.NAMED) { + continue; + } + if (param.name == 'map' && param.type.isAssignableTo(mapType)) { + return true; + } + } + return false; + }); + + if (acceptableAnnotations.isEmpty) { + warn('Could not find a constructor for member annotations in ' + '$ngAnnotations', type.type); + return; + } + + // Default to using the first acceptable annotation- not sure if + // more than one should ever occur. + var annotation = acceptableAnnotations.first; + var mapArg = annotation.arguments.arguments.firstWhere((arg) => + (arg is NamedExpression) && (arg.name.label.name == 'map'), + orElse: () => null); + + // If we don't have a 'map' parameter yet, add one. + if (mapArg == null) { + var map = new MapLiteral(null, null, null, [], null); + var label = new Label(new SimpleIdentifier(stringToken('map')), + new _GeneratedToken(TokenType.COLON, ':')); + mapArg = new NamedExpression(label, map); + annotation.arguments.arguments.add(mapArg); + } + + var map = mapArg.expression; + if (map is! MapLiteral) { + warn('Expected \'map\' argument of $annotation to be a map literal', + type.type); + return; + } + memberAnnotations.forEach((memberName, annotation) { + var key = annotation.arguments.arguments.first; + // If the key already exists then it means we have two annotations for + // same member. + if (map.entries.any((entry) => entry.key.toString() == key.toString())) { + warn('Directive $annotation already contains an entry for $key', + type.type); + return; + } + + var typeName = annotation.element.enclosingElement.name; + var value = '${_annotationToMapping[typeName]}$memberName'; + var entry = new MapLiteralEntry( + key, + new _GeneratedToken(TokenType.COLON, ':'), + new SimpleStringLiteral(stringToken(value), value)); + map.entries.add(entry); + }); + } + + Token stringToken(String str) => new _GeneratedToken(TokenType.STRING, str); + + void warn(String msg, Element element) { + logger.warning(msg, asset: resolver.getSourceAssetId(element), + span: resolver.getSourceSpan(element)); + } +} + +/// Subclass for tokens which we're generating here. +class _GeneratedToken extends Token { + final String lexeme; + _GeneratedToken(TokenType type, this.lexeme) : super(type, 0); +} + + +/** + * AST visitor which walks the current AST and finds all annotated + * classes and members. + */ +class _AnnotationVisitor extends GeneralizingASTVisitor { + final List allowedMemberAnnotations; + final List classAnnotations = []; + final Map> memberAnnotations = {}; + + _AnnotationVisitor(this.allowedMemberAnnotations); + + void visitAnnotation(Annotation annotation) { + var parent = annotation.parent; + if (parent is! Declaration) return; + + if (parent.element is ClassElement) { + classAnnotations.add(annotation); + } else if (allowedMemberAnnotations.contains(annotation.element)) { + if (parent is MethodDeclaration) { + memberAnnotations.putIfAbsent(parent.name.name, () => []) + .add(annotation); + } else if (parent is FieldDeclaration) { + var name = parent.fields.variables.first.name.name; + memberAnnotations.putIfAbsent(name, () => []).add(annotation); + } + } + } + + bool get hasAnnotations => + !classAnnotations.isEmpty || !memberAnnotations.isEmpty; +} diff --git a/lib/tools/transformer/metadata_generator.dart b/lib/tools/transformer/metadata_generator.dart new file mode 100644 index 000000000..0768cf662 --- /dev/null +++ b/lib/tools/transformer/metadata_generator.dart @@ -0,0 +1,121 @@ +library angular.tools.transformer.metadata_generator; + +import 'dart:async'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:path/path.dart' as path; + +import 'metadata_extractor.dart'; + +class MetadataGenerator extends Transformer with ResolverTransformer { + final TransformOptions options; + + MetadataGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => new Future.value( + options.isDartEntry(input.id)); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + var id = asset.id; + var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' + '_static_metadata.dart'; + var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); + var outputId = new AssetId(id.package, outputPath); + + var extractor = new AnnotationExtractor(transform.logger, resolver, + outputId); + + var outputBuffer = new StringBuffer(); + _writeHeader(asset.id, outputBuffer); + + var annotatedTypes = resolver.libraries + .where((lib) => !lib.isInSdk) + .expand((lib) => lib.units) + .expand((unit) => unit.types) + .map(extractor.extractAnnotations) + .where((annotations) => annotations != null).toList(); + + var libs = annotatedTypes.expand((type) => type.referencedLibraries) + .toSet(); + + var importPrefixes = {}; + var index = 0; + for (var lib in libs) { + if (lib.isDartCore) { + importPrefixes[lib] = ''; + continue; + } + + var prefix = 'import_${index++}'; + var url = resolver.getImportUri(lib, from: outputId); + outputBuffer.write('import \'$url\' as $prefix;\n'); + importPrefixes[lib] = '$prefix.'; + } + + _writePreamble(outputBuffer); + + _writeClassPreamble(outputBuffer); + for (var type in annotatedTypes) { + type.writeClassAnnotations( + outputBuffer, transform.logger, resolver, importPrefixes); + } + _writeClassEpilogue(outputBuffer); + + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + transform.addOutput(asset); + } +} + +void _writeHeader(AssetId id, StringSink sink) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_metadata; + +import 'package:angular/angular.dart' show MetadataExtractor; +import 'package:di/di.dart' show Module; + +'''); +} + +void _writePreamble(StringSink sink) { + sink.write(''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = typeAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +'''); +} + +void _writeClassPreamble(StringSink sink) { + sink.write(''' +final Map typeAnnotations = { +'''); +} + +void _writeClassEpilogue(StringSink sink) { + sink.write(''' +}; +'''); +} + +void _writeFooter(StringSink sink) { + sink.write(''' +}; +'''); +} diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart new file mode 100644 index 000000000..ce681013c --- /dev/null +++ b/lib/tools/transformer/options.dart @@ -0,0 +1,58 @@ +library angular.tools.transformer.options; + +import 'dart:async'; + +import 'package:barback/barback.dart'; +import 'package:di/transformer/options.dart' as di; +import 'package:path/path.dart' as path; + +/** Options used by Angular transformers */ +class TransformOptions { + + /** + * The file paths of the primary Dart entry point (main) for the application. + * This is used as the starting point to find all expressions used by the + * application. + */ + final Set dartEntries; + + /** + * List of html file paths which may contain Angular expressions. + * The paths are relative to the package home and are represented using posix + * style, which matches the representation used in asset ids in barback. + */ + final List htmlFiles; + + /** + * Path to the Dart SDK directory, for resolving Dart libraries. + */ + final String sdkDirectory; + + /** + * Template cache path modifiers + */ + final Map templateUriRewrites; + + /** + * Dependency injection options. + */ + final di.TransformOptions diOptions; + + TransformOptions({List dartEntries, + String sdkDirectory, List htmlFiles, + Map templateUriRewrites, + di.TransformOptions diOptions}) + : dartEntries = dartEntries.toSet(), + sdkDirectory = sdkDirectory, + htmlFiles = htmlFiles != null ? htmlFiles : [], + templateUriRewrites = templateUriRewrites != null ? + templateUriRewrites : {}, + diOptions = diOptions { + if (sdkDirectory == null) + throw new ArgumentError('sdkDirectory must be provided.'); + } + + // Don't need to check package as transformers only run for primary package. + Future isDartEntry(AssetId id) => + new Future.value(dartEntries.contains(id.path)); +} diff --git a/lib/tools/transformer/static_angular_generator.dart b/lib/tools/transformer/static_angular_generator.dart new file mode 100644 index 000000000..5b0132571 --- /dev/null +++ b/lib/tools/transformer/static_angular_generator.dart @@ -0,0 +1,101 @@ +library angular.tools.transformer.static_angular_generator; + +import 'dart:async'; +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:barback/barback.dart'; +import 'package:path/path.dart' as path; +import 'package:source_maps/refactor.dart' show TextEditTransaction; + +class StaticAngularGenerator extends Transformer with ResolverTransformer { + final TransformOptions options; + + StaticAngularGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => new Future.value( + options.isDartEntry(input.id)); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + + var dynamicApp = resolver.getType('angular.dynamic.NgDynamicApp'); + if (dynamicApp == null) { + // No dynamic app imports, exit. + transform.addOutput(transform.primaryInput); + return; + } + + var id = asset.id; + var lib = resolver.entryLibrary; + var transaction = resolver.createTextEditTransaction(lib); + var unit = lib.definingCompilationUnit.node; + + for (var directive in unit.directives) { + if (directive is ImportDirective && + directive.uri.stringValue == 'package:angular/angular_dynamic.dart') { + var uri = directive.uri; + transaction.edit(uri.beginToken.offset, uri.end, + '\'package:angular/angular_static.dart\''); + } + } + + var dynamicToStatic = new _NgDynamicToStaticVisitor( + dynamicApp.unnamedConstructor, transaction); + unit.accept(dynamicToStatic); + + var generatedFilePrefix = '${path.url.basenameWithoutExtension(id.path)}'; + _addImport(transaction, unit, + '${generatedFilePrefix}_static_expressions.dart', + 'generated_static_expressions'); + _addImport(transaction, unit, + '${generatedFilePrefix}_static_metadata.dart', + 'generated_static_metadata'); + _addImport(transaction, unit, + '${generatedFilePrefix}_static_injector.dart', + 'generated_static_injector'); + + var printer = transaction.commit(); + var url = id.path.startsWith('lib/') + ? 'package:${id.package}/${id.path.substring(4)}' : id.path; + printer.build(url); + transform.addOutput(new Asset.fromString(id, printer.text)); + } +} + +/// Injects an import into the list of imports in the file. +void _addImport(TextEditTransaction transaction, CompilationUnit unit, + String uri, String prefix) { + var last = unit.directives.where((d) => d is ImportDirective).last; + transaction.edit(last.end, last.end, '\nimport \'$uri\' as $prefix;'); +} + +class _NgDynamicToStaticVisitor extends GeneralizingASTVisitor { + final ConstructorElement constructor; + final TextEditTransaction transaction; + _NgDynamicToStaticVisitor(this.constructor, this.transaction); + + visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.staticElement == constructor) { + var ctorName = node.constructorName; + var ctorStr = ctorName.toString(); + var prefix = ctorStr.indexOf('.') == -1 ? '' : + ctorStr.substring(0, ctorStr.indexOf('.') + 1); + + transaction.edit(ctorName.beginToken.offset, ctorName.end, + '${prefix}NgStaticApp'); + + var args = node.argumentList; + transaction.edit(args.beginToken.offset + 1, args.end - 1, + 'generated_static_injector.factories, ' + 'generated_static_metadata.typeAnnotations, ' + 'generated_static_expressions.getters, ' + 'new generated_static_expressions.StaticClosureMap()'); + } + return super.visitNode(node); + } +} diff --git a/lib/transformer.dart b/lib/transformer.dart new file mode 100644 index 000000000..43a2648ed --- /dev/null +++ b/lib/transformer.dart @@ -0,0 +1,134 @@ +library angular.transformer; + +import 'dart:io'; +import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:angular/tools/transformer/static_angular_generator.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/injector_generator.dart' as di; +import 'package:di/transformer/options.dart' as di; +import 'package:path/path.dart' as path; + + + /** + * The Angular transformer, which internally runs several phases that will: + * + * * Extract all expressions for evaluation at runtime without using Mirrors. + * * Extract all classes being dependency injected into a static injector. + * * Extract all metadata for cached reflection. + */ +class AngularTransformerGroup implements TransformerGroup { + final Iterable phases; + + AngularTransformerGroup(TransformOptions options) + : phases = _createPhases(options); + + AngularTransformerGroup.asPlugin(BarbackSettings settings) + : this(_parseSettings(settings.configuration)); +} + +TransformOptions _parseSettings(Map args) { + // Default angular annotations for injectable types + var annotations = [ + 'angular.core.service.NgInjectableService', + 'angular.core.NgDirective', + 'angular.core.NgController', + 'angular.core.NgComponent', + 'angular.core.NgFilter']; + annotations.addAll(_readStringListValue(args, 'injectable_annotations')); + + var injectedTypes = [ + 'perf_api.Profiler', + 'angular.core.RootScope', + 'angular.core.AstParser', + 'angular.core.dom.NgAnimate']; + injectedTypes.addAll(_readStringListValue(args, 'injected_types')); + + var sdkDir = _readStringValue(args, 'dart_sdk', required: false); + if (sdkDir == null) { + // Assume the Pub executable is always coming from the SDK. + sdkDir = path.dirname(path.dirname(Platform.executable)); + } + + var dartEntries = _readStringListValue(args, 'dart_entries'); + + var diOptions = new di.TransformOptions( + dartEntries: dartEntries, + injectableAnnotations: annotations, + injectedTypes: injectedTypes, + sdkDirectory: sdkDir); + + return new TransformOptions( + dartEntries: dartEntries, + htmlFiles: _readStringListValue(args, 'html_files'), + sdkDirectory: sdkDir, + templateUriRewrites: _readStringMapValue(args, 'template_uri_rewrites'), + diOptions: diOptions); +} + +_readStringValue(Map args, String name, {bool required: true}) { + var value = args[name]; + if (value == null) { + if (required) { + print('Angular transformer parameter "$name" ' + 'has no value in pubspec.yaml.'); + } + return null; + } + if (value is! String) { + print('Angular transformer parameter "$name" value ' + 'is not a string in pubspec.yaml.'); + return null; + } + return value; +} + +_readStringListValue(Map args, String name) { + var value = args[name]; + if (value == null) return []; + var results = []; + bool error; + if (value is List) { + results = value; + error = value.any((e) => e is! String); + } else if (value is String) { + results = [value]; + error = false; + } else { + error = true; + } + if (error) { + print('Angular transformer parameter "$name" ' + 'has an invalid value in pubspec.yaml.'); + } + return results; +} + +Map _readStringMapValue(Map args, String name) { + var value = args[name]; + if (value == null) return {}; + if (value is! Map) { + print('Angular transformer parameter "$name" ' + 'is expected to be a map parameter.'); + return {}; + } + if (value.keys.any((e) => e is! String) || + value.values.any((e) => e is! String)) { + print('Angular transformer parameter "$name" ' + 'is expected to be a map of strings.'); + return {}; + } + return value; +} + +List> _createPhases(TransformOptions options) { + var resolvers = new Resolvers(options.sdkDirectory); + return [ + [new ExpressionGenerator(options, resolvers)], + [new di.InjectorGenerator(options.diOptions, resolvers)], + [new MetadataGenerator(options, resolvers)], + [new StaticAngularGenerator(options, resolvers)], + ]; +} diff --git a/perf/invoke_perf.dart b/perf/invoke_perf.dart index 09c45fd46..524b2a56f 100644 --- a/perf/invoke_perf.dart +++ b/perf/invoke_perf.dart @@ -1,31 +1,132 @@ library angular.perf.invoke; -import '_perf.dart'; -import 'dart:async'; +import 'package:benchmark_harness/benchmark_harness.dart'; main() { - var handleDirect = (a, b, c) => a; - var wrap = new Wrap(); - var handleDirectNamed = ({a, b, c}) => a; - var handleIndirect = (e) => e.a; - var streamC = new StreamController(sync:true); - var stream = streamC.stream..listen(handleIndirect); - - time('direct', () => handleDirect(1, 2, 3) ); - time('.call', () => wrap(1, 2, 3) ); - time('directNamed', () => handleDirectNamed(a:1, b:2, c:3) ); - time('indirect', () => handleIndirect(new Container(1, 2, 3)) ); - time('stream', () => streamC.add(new Container(1, 2, 3))); + new MonomorphicClosure0ListInvoke().report(); + new PolymorphicMethod0ListInvoke().report(); + new PolymorphicClosure0ListInvoke().report(); + new PolymorphicMethod1ListInvoke().report(); + new PolymorphicClosure1ListInvoke().report(); } -class Container { - var a; - var b; - var c; +var closure0Factory = [ + () => () => 0, + () => () => 1, + () => () => 2, + () => () => 3, + () => () => 4, + () => () => 5, + () => () => 6, + () => () => 7, + () => () => 8, + () => () => 9, +]; - Container(this.a, this.b, this.c); +var closure1Factory = [ + () => (i) => 0, + () => (i) => 1, + () => (i) => 2, + () => (i) => 3, + () => (i) => 4, + () => (i) => 5, + () => (i) => 6, + () => (i) => 7, + () => (i) => 8, + () => (i) => 9, +]; + +var instanceFactory = [ + () => new Obj0(), + () => new Obj1(), + () => new Obj2(), + () => new Obj3(), + () => new Obj4(), + () => new Obj5(), + () => new Obj6(), + () => new Obj7(), + () => new Obj8(), + () => new Obj9(), +]; + +class Obj0 { method0() => 0; method1(i) => 0; } +class Obj1 { method0() => 1; method1(i) => 1; } +class Obj2 { method0() => 2; method1(i) => 2; } +class Obj3 { method0() => 3; method1(i) => 3; } +class Obj4 { method0() => 4; method1(i) => 4; } +class Obj5 { method0() => 5; method1(i) => 5; } +class Obj6 { method0() => 6; method1(i) => 6; } +class Obj7 { method0() => 7; method1(i) => 7; } +class Obj8 { method0() => 8; method1(i) => 8; } +class Obj9 { method0() => 9; method1(i) => 9; } + +class PolymorphicClosure0ListInvoke extends BenchmarkBase { + PolymorphicClosure0ListInvoke() : super('PolymorphicClosure0ListInvoke'); + + var list = new List.generate(10000, (i) => closure0Factory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i](); + } + return sum; + } +} + +class MonomorphicClosure0ListInvoke extends BenchmarkBase { + MonomorphicClosure0ListInvoke() : super('MonomorphicClosure0ListInvoke'); + + var list = new List.generate(10000, (i) => closure0Factory[0]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i](); + } + return sum; + } +} + +class PolymorphicClosure1ListInvoke extends BenchmarkBase { + PolymorphicClosure1ListInvoke() : super('PolymorphicClosure1ListInvoke'); + + var list = new List.generate(10000, (i) => closure1Factory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i](i); + } + return sum; + } +} + +class PolymorphicMethod0ListInvoke extends BenchmarkBase { + PolymorphicMethod0ListInvoke() : super('PolymorphicMethod0ListInvoke'); + + var list = new List.generate(10000, (i) => instanceFactory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i].method0(); + } + return sum; + } } -class Wrap { - call(a, b, c) => a + b + c; +class PolymorphicMethod1ListInvoke extends BenchmarkBase { + PolymorphicMethod1ListInvoke() : super('PolymorphicMethod1ListInvoke'); + + var list = new List.generate(10000, (i) => instanceFactory[i%10]()); + + run() { + int sum = 0; + for(var i=0; i < list.length; i++) { + sum += list[i].method1(i); + } + return sum; + } } + diff --git a/perf/mirror_perf.dart b/perf/mirror_perf.dart deleted file mode 100644 index 478906040..000000000 --- a/perf/mirror_perf.dart +++ /dev/null @@ -1,65 +0,0 @@ -library angular.perf.mirror; - -import '_perf.dart'; -import 'dart:mirrors'; -import 'package:angular/change_detection/dirty_checking_change_detector.dart'; - -main() { - var c = new _Obj(1); - InstanceMirror im = reflect(c); - Symbol symbol = const Symbol('a'); - _Watch head = new _Watch(); - _Watch current = head; - GetterCache getterCache = new GetterCache({}); - var detector = new DirtyCheckingChangeDetector(getterCache); - for(var i=1; i < 10000; i++) { - _Watch next = new _Watch(); - current = (current.next = new _Watch()); - detector.watch(c, 'a', ''); - } - - var dirtyCheck = () { - _Watch current = head; - while(current != null) { - if (!identical(current.lastValue, current.im.getField(current.symbol).reflectee)) { - throw "We should not get here"; - } - current = current.next; - } - }; - - var dirtyCheckFn = () { - _Watch current = head; - while(current != null) { - if (!identical(current.lastValue, current.getter(current.object))) { - throw "We should not get here"; - } - current = current.next; - } - }; - - xtime('fieldRead', () => im.getField(symbol).reflectee ); - xtime('Object.observe', dirtyCheck); - xtime('Object.observe fn()', dirtyCheckFn); - time('ChangeDetection', detector.collectChanges); -} - -class _Watch { - dynamic lastValue = 1; - _Watch next; - String location; - dynamic object = new _Obj(1); - InstanceMirror im; - Symbol symbol = const Symbol('a'); - Function getter = (s) => s.a; - - _Watch() { - im = reflect(object); - } -} - -class _Obj { - var a; - - _Obj(this.a); -} diff --git a/perf/watch_group_perf.dart b/perf/watch_group_perf.dart index bfc2ab6a5..6f2fcec24 100644 --- a/perf/watch_group_perf.dart +++ b/perf/watch_group_perf.dart @@ -2,6 +2,8 @@ library angular.perf.watch_group; import '_perf.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; import 'package:angular/change_detection/watch_group.dart'; import 'package:benchmark_harness/benchmark_harness.dart'; @@ -14,7 +16,14 @@ import 'package:benchmark_harness/benchmark_harness.dart'; import 'dart:mirrors' show MirrorsUsed; var _reactionFn = (_, __) => null; -var _getterCache = new GetterCache({}); +var _staticFieldGetterFactory = new StaticFieldGetterFactory({ + "a": (o) => o.a, "b": (o) => o.b, "c": (o) => o.c, "d": (o) => o.d, "e": (o) => o.e, + "f": (o) => o.f, "g": (o) => o.g, "h": (o) => o.h, "i": (o) => o.i, "j": (o) => o.j, + "k": (o) => o.k, "l": (o) => o.l, "m": (o) => o.m, "n": (o) => o.n, "o": (o) => o.o, + "p": (o) => o.p, "q": (o) => o.q, "r": (o) => o.r, "s": (o) => o.s, "t": (o) => o.t, +}); +var _dynamicFieldGetterFactory = new DynamicFieldGetterFactory(); + main() { _fieldRead(); _fieldReadGetter(); @@ -27,7 +36,7 @@ main() { class _CollectionCheck extends BenchmarkBase { List list = new List.generate(1000, (i) => i); - var detector = new DirtyCheckingChangeDetector(_getterCache); + var detector = new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory); _CollectionCheck(): super('change-detect List[1000]') { detector @@ -41,8 +50,8 @@ class _CollectionCheck extends BenchmarkBase { } _fieldRead() { - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), new _Obj()) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), new _Obj()) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -70,14 +79,8 @@ _fieldRead() { } _fieldReadGetter() { - var getterCache = new GetterCache({ - "a": (o) => o.a, "b": (o) => o.b, "c": (o) => o.c, "d": (o) => o.d, "e": (o) => o.e, - "f": (o) => o.f, "g": (o) => o.g, "h": (o) => o.h, "i": (o) => o.i, "j": (o) => o.j, - "k": (o) => o.k, "l": (o) => o.l, "m": (o) => o.m, "n": (o) => o.n, "o": (o) => o.o, - "p": (o) => o.p, "q": (o) => o.q, "r": (o) => o.r, "s": (o) => o.s, "t": (o) => o.t, - }); - var watchGrp= new RootWatchGroup( - new DirtyCheckingChangeDetector(getterCache), new _Obj()) + var watchGrp= new RootWatchGroup(_staticFieldGetterFactory, + new DirtyCheckingChangeDetector(_staticFieldGetterFactory), new _Obj()) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -110,8 +113,8 @@ _mapRead() { 'f': 0, 'g': 1, 'h': 2, 'i': 3, 'j': 4, 'k': 0, 'l': 1, 'm': 2, 'n': 3, 'o': 4, 'p': 0, 'q': 1, 'r': 2, 's': 3, 't': 4}; - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), map) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), map) ..watch(_parse('a'), _reactionFn) ..watch(_parse('b'), _reactionFn) ..watch(_parse('c'), _reactionFn) @@ -140,8 +143,8 @@ _mapRead() { _methodInvoke0() { var context = new _Obj(); context.a = new _Obj(); - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), context) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) ..watch(_method('a', 'methodA'), _reactionFn) ..watch(_method('a', 'methodB'), _reactionFn) ..watch(_method('a', 'methodC'), _reactionFn) @@ -170,8 +173,8 @@ _methodInvoke0() { _methodInvoke1() { var context = new _Obj(); context.a = new _Obj(); - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), context) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) ..watch(_method('a', 'methodA1', [_parse('a')]), _reactionFn) ..watch(_method('a', 'methodB1', [_parse('a')]), _reactionFn) ..watch(_method('a', 'methodC1', [_parse('a')]), _reactionFn) @@ -199,8 +202,8 @@ _methodInvoke1() { _function2() { var context = new _Obj(); - var watchGrp = new RootWatchGroup( - new DirtyCheckingChangeDetector(_getterCache), context) + var watchGrp = new RootWatchGroup(_dynamicFieldGetterFactory, + new DirtyCheckingChangeDetector(_dynamicFieldGetterFactory), context) ..watch(_add(0, _parse('a'), _parse('a')), _reactionFn) ..watch(_add(1, _parse('a'), _parse('a')), _reactionFn) ..watch(_add(2, _parse('a'), _parse('a')), _reactionFn) diff --git a/pubspec.lock b/pubspec.lock index 7b1f2bc3c..43f30bb74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -4,11 +4,15 @@ packages: analyzer: description: analyzer source: hosted - version: "0.11.10" + version: "0.12.2" args: description: args source: hosted version: "0.9.0" + barback: + description: barback + source: hosted + version: "0.11.1" benchmark_harness: description: benchmark_harness source: hosted @@ -17,13 +21,20 @@ packages: description: browser source: hosted version: "0.9.1" + code_transformers: + description: code_transformers + source: hosted + version: "0.0.1-dev.2" collection: description: collection source: hosted version: "0.9.1" di: - description: di - source: hosted + description: + ref: null + resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" + url: "https://github.com/angular/di.dart.git" + source: git version: "0.0.33" html5lib: description: html5lib @@ -32,11 +43,15 @@ packages: intl: description: intl source: hosted - version: "0.9.6" + version: "0.8.10+4" logging: description: logging source: hosted version: "0.9.1+1" + meta: + description: meta + source: hosted + version: "0.8.8" path: description: path source: hosted diff --git a/pubspec.yaml b/pubspec.yaml index 2668738e7..d349e1111 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,8 +10,9 @@ homepage: https://angulardart.org environment: sdk: '>=1.2.0' dependencies: - analyzer: '>=0.10.0 <0.12.0' + analyzer: '>=0.12.0 <0.13.0' browser: '>=0.8.7 <0.10.0' + code_transformers: '>=0.0.1-dev.2 <0.1.0' collection: '>=0.9.1 <1.0.0' di: '>=0.0.32 <0.1.0' html5lib: '>=0.8.7 <0.10.0' @@ -22,3 +23,7 @@ dependencies: dev_dependencies: benchmark_harness: '>=1.0.0' unittest: '>=0.8.7 <0.10.0' + +dependency_overrides: + di: + git: https://github.com/angular/di.dart.git diff --git a/test/_specs.dart b/test/_specs.dart index f6f63d8d0..0896ac1e6 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -4,6 +4,7 @@ import 'dart:html'; import 'package:unittest/unittest.dart' as unit; import 'package:angular/angular.dart'; import 'package:angular/mock/module.dart'; +import 'package:angular/mock/test_injection.dart'; import 'package:collection/wrappers.dart' show DelegatingList; import 'jasmine_syntax.dart'; @@ -16,6 +17,7 @@ export 'package:di/dynamic_injector.dart'; export 'package:angular/angular.dart'; export 'package:angular/animate/module.dart'; export 'package:angular/mock/module.dart'; +export 'package:angular/mock/test_injection.dart'; export 'package:perf_api/perf_api.dart'; es(String html) { diff --git a/test/bootstrap_spec.dart b/test/bootstrap_spec.dart index b74e7b263..f0c6e5dee 100644 --- a/test/bootstrap_spec.dart +++ b/test/bootstrap_spec.dart @@ -1,6 +1,7 @@ library bootstrap_spec; import '_specs.dart'; +import 'package:angular/angular_dynamic.dart'; void main() { describe('bootstrap', () { @@ -8,7 +9,7 @@ void main() { it('should default to whole page', () { body.innerHtml = '
{{"works"}}
'; - ngBootstrap(); + new NgDynamicApp().run(); expect(body.innerHtml).toEqual('
works
'); }); @@ -16,7 +17,7 @@ void main() { body.setInnerHtml( '
{{ignor me}}
', treeSanitizer: new NullTreeSanitizer()); - ngBootstrap(); + new NgDynamicApp().run(); expect(body.text).toEqual('{{ignor me}}works'); }); @@ -24,7 +25,7 @@ void main() { body.setInnerHtml( '
{{ignor me}}
', treeSanitizer: new NullTreeSanitizer()); - ngBootstrap(element:body.querySelector('div[ng-bind]')); + new NgDynamicApp()..selector('div[ng-bind]')..run(); expect(body.text).toEqual('{{ignor me}}works'); }); }); diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 8f54dca37..f975d32cf 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -1,515 +1,523 @@ -library dirty_chekcing_change_detector_spec; +library dirty_checking_change_detector_spec; import '../_specs.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; import 'dart:collection'; void main() { - describe('DirtyCheckingChangeDetector', () { - DirtyCheckingChangeDetector detector; + var staticFieldGetterFactory = new StaticFieldGetterFactory({ + 'first': (o) => o.first, + 'last': (o) => o.last, + 'age': (o) => o.age, + }); - beforeEach(() { - GetterCache getterCache = new GetterCache({ - "first": (o) => o.first, - "age": (o) => o.age - }); - detector = new DirtyCheckingChangeDetector(getterCache); - }); + they(FieldGetterFactory fieldGetterFactory, String modeName) { + describe('DirtyCheckingChangeDetector-$modeName', () { + DirtyCheckingChangeDetector detector; - describe('object field', () { - it('should detect nothing', () { - var changes = detector.collectChanges(); - expect(changes.moveNext()).toEqual(false); + beforeEach(() { + detector = new DirtyCheckingChangeDetector(fieldGetterFactory); }); - it('should detect field changes', () { - var user = new _User('', ''); - Iterator changeIterator; + describe('object field', () { + it('should detect nothing', () { + var changes = detector.collectChanges(); + expect(changes.moveNext()).toEqual(false); + }); + + it('should detect field changes', () { + var user = new _User('', ''); + Iterator changeIterator; - detector + detector ..watch(user, 'first', null) ..watch(user, 'last', null) ..collectChanges(); // throw away first set - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - user..first = 'misko' + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + user..first = 'misko' ..last = 'hevery'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('misko'); - expect(changeIterator.current.previousValue).toEqual(''); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('hevery'); - expect(changeIterator.current.previousValue).toEqual(''); - expect(changeIterator.moveNext()).toEqual(false); - - // force different instance - user.first = 'mis'; - user.first += 'ko'; - - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - - user.last = 'Hevery'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('Hevery'); - expect(changeIterator.current.previousValue).toEqual('hevery'); - expect(changeIterator.moveNext()).toEqual(false); - }); - - it('should ignore NaN != NaN', () { - var user = new _User(); - user.age = double.NAN; - detector..watch(user, 'age', null)..collectChanges(); // throw away first set - - var changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - - user.age = 123; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual(123); - expect(changeIterator.current.previousValue.isNaN).toEqual(true); - expect(changeIterator.moveNext()).toEqual(false); - }); - - it('should treat map field dereference as []', () { - var obj = {'name':'misko'}; - detector.watch(obj, 'name', null); - detector.collectChanges(); // throw away first set - - obj['name'] = 'Misko'; - var changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue).toEqual('Misko'); - expect(changeIterator.current.previousValue).toEqual('misko'); - }); - }); - - describe('insertions / removals', () { - it('should insert at the end of list', () { - var obj = {}; - var a = detector.watch(obj, 'a', 'a'); - var b = detector.watch(obj, 'b', 'b'); - - obj['a'] = obj['b'] = 1; - var changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.handler).toEqual('a'); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.handler).toEqual('b'); - expect(changeIterator.moveNext()).toEqual(false); - - obj['a'] = obj['b'] = 2; - a.remove(); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.handler).toEqual('b'); - expect(changeIterator.moveNext()).toEqual(false); - - obj['a'] = obj['b'] = 3; - b.remove(); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(false); - }); - - it('should remove all watches in group and group\'s children', () { - var obj = {}; - detector.watch(obj, 'a', '0a'); - var child1a = detector.newGroup(); - var child1b = detector.newGroup(); - var child2 = child1a.newGroup(); - child1a.watch(obj,'a', '1a'); - child1b.watch(obj,'a', '1b'); - detector.watch(obj, 'a', '0A'); - child1a.watch(obj,'a', '1A'); - child2.watch(obj,'a', '2A'); - - var iterator; - obj['a'] = 1; - expect(detector.collectChanges(), - toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); - - obj['a'] = 2; - child1a.remove(); // should also remove child2 - expect(detector.collectChanges(), toEqualChanges(['0a', '0A', '1b'])); - }); - - it('should add watches within its own group', () { - var obj = {}; - var ra = detector.watch(obj, 'a', 'a'); - var child = detector.newGroup(); - var cb = child.watch(obj,'b', 'b'); - var iterotar; - - obj['a'] = obj['b'] = 1; - expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); - - obj['a'] = obj['b'] = 2; - ra.remove(); - expect(detector.collectChanges(), toEqualChanges(['b'])); - - obj['a'] = obj['b'] = 3; - cb.remove(); - expect(detector.collectChanges(), toEqualChanges([])); - - // TODO: add them back in wrong order, assert events in right order - cb = child.watch(obj,'b', 'b'); - ra = detector.watch(obj, 'a', 'a'); - obj['a'] = obj['b'] = 4; - expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); - }); - - it('should properly add children', () { - var a = detector.newGroup(); - var aChild = a.newGroup(); - var b = detector.newGroup(); - expect(detector.collectChanges).not.toThrow(); - }); - }); - - describe('list watching', () { - it('should detect changes in list', () { - var list = []; - var record = detector.watch(list, null, 'handler'); - expect(detector.collectChanges().moveNext()).toEqual(false); - var iterator; - - list.add('a'); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a[null -> 0]'], - additions: ['a[null -> 0]'], - moves: [], - removals: [])); - - list.add('b'); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]'], - additions: ['b[null -> 1]'], - moves: [], - removals: [])); - - list.add('c'); - list.add('d'); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], - additions: ['c[null -> 2]', 'd[null -> 3]'], - moves: [], - removals: [])); - - list.remove('c'); - expect(list).toEqual(['a', 'b', 'd']); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'd[3 -> 2]'], - additions: [], - moves: ['d[3 -> 2]'], - removals: ['c[2 -> null]'])); - - list.clear(); - list.addAll(['d', 'c', 'b', 'a']); - iterator = detector.collectChanges(); - expect(iterator.moveNext()).toEqual(true); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], - additions: ['c[null -> 1]'], - moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], - removals: [])); - }); - - it('should detect changes in list', () { - var list = []; - var record = detector.watch(list.map((i) => i), null, 'handler'); - expect(detector.collectChanges().moveNext()).toEqual(false); - var iterator; - - list.add('a'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a[null -> 0]'], - additions: ['a[null -> 0]'], - moves: [], - removals: [])); - - list.add('b'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]'], - additions: ['b[null -> 1]'], - moves: [], - removals: [])); - - list.add('c'); - list.add('d'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], - additions: ['c[null -> 2]', 'd[null -> 3]'], - moves: [], - removals: [])); - - list.remove('c'); - expect(list).toEqual(['a', 'b', 'd']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'd[3 -> 2]'], - additions: [], - moves: ['d[3 -> 2]'], - removals: ['c[2 -> null]'])); - - list.clear(); - list.addAll(['d', 'c', 'b', 'a']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], - additions: ['c[null -> 1]'], - moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], - removals: [])); - }); - - it('should test string by value rather than by reference', () { - var list = ['a', 'boo']; - detector..watch(list, null, null)..collectChanges(); - - list[1] = 'b' + 'oo'; - - expect(detector.collectChanges().moveNext()).toEqual(false); - }); - - it('should ignore [NaN] != [NaN]', () { - var list = [double.NAN]; - var record = detector..watch(list, null, null)..collectChanges(); - - expect(detector.collectChanges().moveNext()).toEqual(false); - }); - - it('should remove and add same item', () { - var list = ['a', 'b', 'c']; - var record = detector.watch(list, null, 'handler'); - var iterator; - detector.collectChanges(); - - list.remove('b'); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'c[2 -> 1]'], - additions: [], - moves: ['c[2 -> 1]'], - removals: ['b[1 -> null]'])); - - list.insert(1, 'b'); - expect(list).toEqual(['a', 'b', 'c']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]', 'c[1 -> 2]'], - additions: ['b[null -> 1]'], - moves: ['c[1 -> 2]'], - removals: [])); - }); - - it('should support duplicates', () { - var list = ['a', 'a', 'a', 'b', 'b']; - var record = detector.watch(list, null, 'handler'); - detector.collectChanges(); - - list.removeAt(0); - var iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['a', 'a', 'b[3 -> 2]', 'b[4 -> 3]'], - additions: [], - moves: ['b[3 -> 2]', 'b[4 -> 3]'], - removals: ['a[2 -> null]'])); - }); - - - it('should support insertions/moves', () { - var list = ['a', 'a', 'b', 'b']; - var record = detector.watch(list, null, 'handler'); - var iterator; - detector.collectChanges(); - list.insert(0, 'b'); - expect(list).toEqual(['b', 'a', 'a', 'b', 'b']); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], - additions: ['b[null -> 4]'], - moves: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], - removals: [])); - }); - - it('should support UnmodifiableListView', () { - var hiddenList = [1]; - var list = new UnmodifiableListView(hiddenList); - var record = detector.watch(list, null, 'handler'); - var iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['1[null -> 0]'], - additions: ['1[null -> 0]'], - moves: [], - removals: [])); - - // assert no changes detected - expect(detector.collectChanges().moveNext()).toEqual(false); - - // change the hiddenList normally this should trigger change detection - // but because we are wrapped in UnmodifiableListView we see nothing. - hiddenList[0] = 2; - expect(detector.collectChanges().moveNext()).toEqual(false); - }); - - it('should bug', () { - var list = [1, 2, 3, 4]; - var record = detector.watch(list, null, 'handler'); - var iterator; - - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], - additions: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], - moves: [], - removals: [])); - detector.collectChanges(); - - list.removeRange(0, 1); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], - additions: [], - moves: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], - removals: ['1[0 -> null]'])); - - list.insert(0, 1); - iterator = detector.collectChanges()..moveNext(); - expect(iterator.current.currentValue, toEqualCollectionRecord( - collection: ['1[null -> 0]', '2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], - additions: ['1[null -> 0]'], - moves: ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], - removals: [])); - }); - }); - - describe('map watching', () { - it('should do basic map watching', () { - var map = {}; - var record = detector.watch(map, null, 'handler'); - expect(detector.collectChanges().moveNext()).toEqual(false); - - var changeIterator; - map['a'] = 'A'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a[null -> A]'], - additions: ['a[null -> A]'], - changes: [], - removals: [])); - - map['b'] = 'B'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a', 'b[null -> B]'], - additions: ['b[null -> B]'], - changes: [], - removals: [])); - - map['b'] = 'BB'; - map['d'] = 'D'; - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a', 'b[B -> BB]', 'd[null -> D]'], - additions: ['d[null -> D]'], - changes: ['b[B -> BB]'], - removals: [])); - - map.remove('b'); - expect(map).toEqual({'a': 'A', 'd':'D'}); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: ['a', 'd'], - additions: [], - changes: [], - removals: ['b[BB -> null]'])); - - map.clear(); - changeIterator = detector.collectChanges(); - expect(changeIterator.moveNext()).toEqual(true); - expect(changeIterator.current.currentValue, toEqualMapRecord( - map: [], - additions: [], - changes: [], - removals: ['a[A -> null]', 'd[D -> null]'])); - }); - - it('should test string keys by value rather than by reference', () { - var map = {'foo': 0}; - detector..watch(map, null, null)..collectChanges(); - - map['f' + 'oo'] = 0; - - expect(detector.collectChanges().moveNext()).toEqual(false); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('misko'); + expect(changeIterator.current.previousValue).toEqual(''); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('hevery'); + expect(changeIterator.current.previousValue).toEqual(''); + expect(changeIterator.moveNext()).toEqual(false); + + // force different instance + user.first = 'mis'; + user.first += 'ko'; + + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + + user.last = 'Hevery'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('Hevery'); + expect(changeIterator.current.previousValue).toEqual('hevery'); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should ignore NaN != NaN', () { + var user = new _User(); + user.age = double.NAN; + detector..watch(user, 'age', null)..collectChanges(); // throw away first set + + var changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + + user.age = 123; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual(123); + expect(changeIterator.current.previousValue.isNaN).toEqual(true); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should treat map field dereference as []', () { + var obj = {'name':'misko'}; + detector.watch(obj, 'name', null); + detector.collectChanges(); // throw away first set + + obj['name'] = 'Misko'; + var changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue).toEqual('Misko'); + expect(changeIterator.current.previousValue).toEqual('misko'); + }); }); - it('should test string values by value rather than by reference', () { - var map = {'foo': 'bar'}; - detector..watch(map, null, null)..collectChanges(); - - map['foo'] = 'b' + 'ar'; - - expect(detector.collectChanges().moveNext()).toEqual(false); + describe('insertions / removals', () { + it('should insert at the end of list', () { + var obj = {}; + var a = detector.watch(obj, 'a', 'a'); + var b = detector.watch(obj, 'b', 'b'); + + obj['a'] = obj['b'] = 1; + var changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.handler).toEqual('a'); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.handler).toEqual('b'); + expect(changeIterator.moveNext()).toEqual(false); + + obj['a'] = obj['b'] = 2; + a.remove(); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.handler).toEqual('b'); + expect(changeIterator.moveNext()).toEqual(false); + + obj['a'] = obj['b'] = 3; + b.remove(); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should remove all watches in group and group\'s children', () { + var obj = {}; + detector.watch(obj, 'a', '0a'); + var child1a = detector.newGroup(); + var child1b = detector.newGroup(); + var child2 = child1a.newGroup(); + child1a.watch(obj,'a', '1a'); + child1b.watch(obj,'a', '1b'); + detector.watch(obj, 'a', '0A'); + child1a.watch(obj,'a', '1A'); + child2.watch(obj,'a', '2A'); + + var iterator; + obj['a'] = 1; + expect(detector.collectChanges(), + toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); + + obj['a'] = 2; + child1a.remove(); // should also remove child2 + expect(detector.collectChanges(), toEqualChanges(['0a', '0A', '1b'])); + }); + + it('should add watches within its own group', () { + var obj = {}; + var ra = detector.watch(obj, 'a', 'a'); + var child = detector.newGroup(); + var cb = child.watch(obj,'b', 'b'); + var iterotar; + + obj['a'] = obj['b'] = 1; + expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); + + obj['a'] = obj['b'] = 2; + ra.remove(); + expect(detector.collectChanges(), toEqualChanges(['b'])); + + obj['a'] = obj['b'] = 3; + cb.remove(); + expect(detector.collectChanges(), toEqualChanges([])); + + // TODO: add them back in wrong order, assert events in right order + cb = child.watch(obj,'b', 'b'); + ra = detector.watch(obj, 'a', 'a'); + obj['a'] = obj['b'] = 4; + expect(detector.collectChanges(), toEqualChanges(['a', 'b'])); + }); + + it('should properly add children', () { + var a = detector.newGroup(); + var aChild = a.newGroup(); + var b = detector.newGroup(); + expect(detector.collectChanges).not.toThrow(); + }); }); - it('should not see a NaN value as a change', () { - var map = {'foo': double.NAN}; - var record = detector..watch(map, null, null)..collectChanges(); - - expect(detector.collectChanges().moveNext()).toEqual(false); + describe('list watching', () { + it('should detect changes in list', () { + var list = []; + var record = detector.watch(list, null, 'handler'); + expect(detector.collectChanges().moveNext()).toEqual(false); + var iterator; + + list.add('a'); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a[null -> 0]'], + additions: ['a[null -> 0]'], + moves: [], + removals: [])); + + list.add('b'); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b[null -> 1]'], + additions: ['b[null -> 1]'], + moves: [], + removals: [])); + + list.add('c'); + list.add('d'); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], + additions: ['c[null -> 2]', 'd[null -> 3]'], + moves: [], + removals: [])); + + list.remove('c'); + expect(list).toEqual(['a', 'b', 'd']); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'd[3 -> 2]'], + additions: [], + moves: ['d[3 -> 2]'], + removals: ['c[2 -> null]'])); + + list.clear(); + list.addAll(['d', 'c', 'b', 'a']); + iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], + additions: ['c[null -> 1]'], + moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], + removals: [])); + }); + + it('should detect changes in list', () { + var list = []; + var record = detector.watch(list.map((i) => i), null, 'handler'); + expect(detector.collectChanges().moveNext()).toEqual(false); + var iterator; + + list.add('a'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a[null -> 0]'], + additions: ['a[null -> 0]'], + moves: [], + removals: [])); + + list.add('b'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b[null -> 1]'], + additions: ['b[null -> 1]'], + moves: [], + removals: [])); + + list.add('c'); + list.add('d'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], + additions: ['c[null -> 2]', 'd[null -> 3]'], + moves: [], + removals: [])); + + list.remove('c'); + expect(list).toEqual(['a', 'b', 'd']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b', 'd[3 -> 2]'], + additions: [], + moves: ['d[3 -> 2]'], + removals: ['c[2 -> null]'])); + + list.clear(); + list.addAll(['d', 'c', 'b', 'a']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], + additions: ['c[null -> 1]'], + moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], + removals: [])); + }); + + it('should test string by value rather than by reference', () { + var list = ['a', 'boo']; + detector..watch(list, null, null)..collectChanges(); + + list[1] = 'b' + 'oo'; + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should ignore [NaN] != [NaN]', () { + var list = [double.NAN]; + var record = detector..watch(list, null, null)..collectChanges(); + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should remove and add same item', () { + var list = ['a', 'b', 'c']; + var record = detector.watch(list, null, 'handler'); + var iterator; + detector.collectChanges(); + + list.remove('b'); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'c[2 -> 1]'], + additions: [], + moves: ['c[2 -> 1]'], + removals: ['b[1 -> null]'])); + + list.insert(1, 'b'); + expect(list).toEqual(['a', 'b', 'c']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'b[null -> 1]', 'c[1 -> 2]'], + additions: ['b[null -> 1]'], + moves: ['c[1 -> 2]'], + removals: [])); + }); + + it('should support duplicates', () { + var list = ['a', 'a', 'a', 'b', 'b']; + var record = detector.watch(list, null, 'handler'); + detector.collectChanges(); + + list.removeAt(0); + var iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['a', 'a', 'b[3 -> 2]', 'b[4 -> 3]'], + additions: [], + moves: ['b[3 -> 2]', 'b[4 -> 3]'], + removals: ['a[2 -> null]'])); + }); + + + it('should support insertions/moves', () { + var list = ['a', 'a', 'b', 'b']; + var record = detector.watch(list, null, 'handler'); + var iterator; + detector.collectChanges(); + list.insert(0, 'b'); + expect(list).toEqual(['b', 'a', 'a', 'b', 'b']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], + additions: ['b[null -> 4]'], + moves: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], + removals: [])); + }); + + it('should support UnmodifiableListView', () { + var hiddenList = [1]; + var list = new UnmodifiableListView(hiddenList); + var record = detector.watch(list, null, 'handler'); + var iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + + // assert no changes detected + expect(detector.collectChanges().moveNext()).toEqual(false); + + // change the hiddenList normally this should trigger change detection + // but because we are wrapped in UnmodifiableListView we see nothing. + hiddenList[0] = 2; + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should bug', () { + var list = [1, 2, 3, 4]; + var record = detector.watch(list, null, 'handler'); + var iterator; + + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], + additions: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], + moves: [], + removals: [])); + detector.collectChanges(); + + list.removeRange(0, 1); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], + additions: [], + moves: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], + removals: ['1[0 -> null]'])); + + list.insert(0, 1); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]', '2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], + additions: ['1[null -> 0]'], + moves: ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], + removals: [])); + }); }); - }); - describe('DuplicateMap', () { - DuplicateMap map; - beforeEach(() => map = new DuplicateMap()); - - it('should do basic operations', () { - var k1 = 'a'; - var r1 = new ItemRecord(k1)..currentIndex = 1; - map.put(r1); - expect(map.get(k1, 2)).toEqual(null); - expect(map.get(k1, 1)).toEqual(null); - expect(map.get(k1, 0)).toEqual(r1); - expect(map.remove(r1)).toEqual(r1); - expect(map.get(k1, -1)).toEqual(null); + describe('map watching', () { + it('should do basic map watching', () { + var map = {}; + var record = detector.watch(map, null, 'handler'); + expect(detector.collectChanges().moveNext()).toEqual(false); + + var changeIterator; + map['a'] = 'A'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a[null -> A]'], + additions: ['a[null -> A]'], + changes: [], + removals: [])); + + map['b'] = 'B'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a', 'b[null -> B]'], + additions: ['b[null -> B]'], + changes: [], + removals: [])); + + map['b'] = 'BB'; + map['d'] = 'D'; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a', 'b[B -> BB]', 'd[null -> D]'], + additions: ['d[null -> D]'], + changes: ['b[B -> BB]'], + removals: [])); + + map.remove('b'); + expect(map).toEqual({'a': 'A', 'd':'D'}); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: ['a', 'd'], + additions: [], + changes: [], + removals: ['b[BB -> null]'])); + + map.clear(); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.current.currentValue, toEqualMapRecord( + map: [], + additions: [], + changes: [], + removals: ['a[A -> null]', 'd[D -> null]'])); + }); + + it('should test string keys by value rather than by reference', () { + var map = {'foo': 0}; + detector..watch(map, null, null)..collectChanges(); + + map['f' + 'oo'] = 0; + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should test string values by value rather than by reference', () { + var map = {'foo': 'bar'}; + detector..watch(map, null, null)..collectChanges(); + + map['foo'] = 'b' + 'ar'; + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); + + it('should not see a NaN value as a change', () { + var map = {'foo': double.NAN}; + var record = detector..watch(map, null, null)..collectChanges(); + + expect(detector.collectChanges().moveNext()).toEqual(false); + }); }); - it('should do basic operations on duplicate keys', () { - var k1 = 'a'; - var r1 = new ItemRecord(k1)..currentIndex = 1; - var r2 = new ItemRecord(k1)..currentIndex = 2; - map..put(r1)..put(r2); - expect(map.get(k1, 0)).toEqual(r1); - expect(map.get(k1, 1)).toEqual(r2); - expect(map.get(k1, 2)).toEqual(null); - expect(map.remove(r2)).toEqual(r2); - expect(map.get(k1, 0)).toEqual(r1); - expect(map.remove(r1)).toEqual(r1); - expect(map.get(k1, 0)).toEqual(null); + describe('DuplicateMap', () { + DuplicateMap map; + beforeEach(() => map = new DuplicateMap()); + + it('should do basic operations', () { + var k1 = 'a'; + var r1 = new ItemRecord(k1)..currentIndex = 1; + map.put(r1); + expect(map.get(k1, 2)).toEqual(null); + expect(map.get(k1, 1)).toEqual(null); + expect(map.get(k1, 0)).toEqual(r1); + expect(map.remove(r1)).toEqual(r1); + expect(map.get(k1, -1)).toEqual(null); + }); + + it('should do basic operations on duplicate keys', () { + var k1 = 'a'; + var r1 = new ItemRecord(k1)..currentIndex = 1; + var r2 = new ItemRecord(k1)..currentIndex = 2; + map..put(r1)..put(r2); + expect(map.get(k1, 0)).toEqual(r1); + expect(map.get(k1, 1)).toEqual(r2); + expect(map.get(k1, 2)).toEqual(null); + expect(map.remove(r2)).toEqual(r2); + expect(map.get(k1, 0)).toEqual(r1); + expect(map.remove(r1)).toEqual(r1); + expect(map.get(k1, 0)).toEqual(null); + }); }); }); - }); + } + they(new DynamicFieldGetterFactory(), 'dynamic'); + they(staticFieldGetterFactory, 'static'); } class _User { diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index b18aaa2d7..cb905537f 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -1,694 +1,706 @@ library watch_group_spec; import '../_specs.dart'; -import 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; import 'dirty_checking_change_detector_spec.dart' hide main; void main() { - describe('WatchGroup', () { - var context; - var watchGrp; - DirtyCheckingChangeDetector changeDetector; - Logger logger; - - AST parse(String expression) { - var currentAST = new ContextReferenceAST(); - expression.split('.').forEach((name) { - currentAST = new FieldReadAST(currentAST, name); - }); - return currentAST; - } - - expectOrder(list) { - logger.clear(); - watchGrp.detectChanges(); // Clear the initial queue - logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(list); - } - - beforeEach(inject((Logger _logger) { - context = {}; - changeDetector = new DirtyCheckingChangeDetector(new GetterCache({})); - watchGrp = new RootWatchGroup(changeDetector, context); - logger = _logger; - })); - - describe('watch lifecycle', () { - it('should prevent reaction fn on removed', () { - context['a'] = 'hello'; - var watch ; - watchGrp.watch(parse('a'), (v, p) { - logger('removed'); - watch.remove(); - }); - watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); - watchGrp.detectChanges(); - expect(logger).toEqual(['removed']); - }); - }); - - describe('property chaining', () { - it('should read property', () { - context['a'] = 'hello'; - - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); - expect(watch.expression).toEqual('a'); - expect(watchGrp.fieldCost).toEqual(1); - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // make sore no new changes are logged on extra detectChanges - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // Should detect value change - context['a'] = 'bye'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'bye']); - - // should cleanup after itself - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - context['a'] = 'cant see me'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'bye']); - }); - - it('should read property chain', () { - context['a'] = {'b': 'hello'}; - - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - expect(changeDetector.count).toEqual(0); - var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); - expect(watch.expression).toEqual('a.b'); - expect(watchGrp.fieldCost).toEqual(2); - expect(changeDetector.count).toEqual(2); - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // make sore no new changes are logged on extra detectChanges - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // make sure no changes or logged when intermediary object changes - context['a'] = {'b': 'hello'}; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello']); - - // Should detect value change - context['a'] = {'b': 'hello2'}; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'hello2']); - - // Should detect value change - context['a']['b'] = 'bye'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'hello2', 'bye']); - - // should cleanup after itself - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - context['a']['b'] = 'cant see me'; - watchGrp.detectChanges(); - expect(logger).toEqual(['hello', 'hello2', 'bye']); - }); - - it('should reuse handlers', () { - var user1 = {'first': 'misko', 'last': 'hevery'}; - var user2 = {'first': 'misko', 'last': 'Hevery'}; - - context['user'] = user1; + var staticFieldGetterFactory = new StaticFieldGetterFactory({ + 'methodA': (o) => o.methodA, + 'toUpperCase': (o) => o.toUpperCase, + 'toLowerCase': (o) => o.toLowerCase, + 'count': (o) => o.count, + }); - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); - var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); - var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); - expect(watchGrp.fieldCost).toEqual(3); + they(FieldGetterFactory fieldGetterFactory, String modeName) { + describe('WatchGroup-$modeName', () { + var context; + var watchGrp; + DirtyCheckingChangeDetector changeDetector; + Logger logger; + + AST parse(String expression) { + var currentAST = new ContextReferenceAST(); + expression.split('.').forEach((name) { + currentAST = new FieldReadAST(currentAST, name); + }); + return currentAST; + } - watchGrp.detectChanges(); - expect(logger).toEqual([user1, 'misko', 'hevery']); + expectOrder(list) { + logger.clear(); + watchGrp.detectChanges(); // Clear the initial queue logger.clear(); - - context['user'] = user2; watchGrp.detectChanges(); - expect(logger).toEqual([user2, 'Hevery']); + expect(logger).toEqual(list); + } + + beforeEach(inject((Logger _logger) { + context = {}; + changeDetector = new DirtyCheckingChangeDetector(fieldGetterFactory); + watchGrp = new RootWatchGroup(fieldGetterFactory, changeDetector, context); + logger = _logger; + })); + + describe('watch lifecycle', () { + it('should prevent reaction fn on removed', () { + context['a'] = 'hello'; + var watch ; + watchGrp.watch(parse('a'), (v, p) { + logger('removed'); + watch.remove(); + }); + watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['removed']); + }); + }); + describe('property chaining', () { + it('should read property', () { + context['a'] = 'hello'; - watch.remove(); - expect(watchGrp.fieldCost).toEqual(3); + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); + expect(watch.expression).toEqual('a'); + expect(watchGrp.fieldCost).toEqual(1); + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); - watchFirst.remove(); - expect(watchGrp.fieldCost).toEqual(2); + // make sore no new changes are logged on extra detectChanges + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); - watchLast.remove(); - expect(watchGrp.fieldCost).toEqual(0); + // Should detect value change + context['a'] = 'bye'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'bye']); - expect(() => watch.remove()).toThrow('Already deleted!'); - }); + // should cleanup after itself + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + context['a'] = 'cant see me'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'bye']); + }); - it('should eval pure FunctionApply', () { - context['a'] = {'val': 1}; + it('should read property chain', () { + context['a'] = {'b': 'hello'}; + + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + expect(changeDetector.count).toEqual(0); + var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); + expect(watch.expression).toEqual('a.b'); + expect(watchGrp.fieldCost).toEqual(2); + expect(changeDetector.count).toEqual(2); + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); + + // make sore no new changes are logged on extra detectChanges + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); + + // make sure no changes or logged when intermediary object changes + context['a'] = {'b': 'hello'}; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello']); + + // Should detect value change + context['a'] = {'b': 'hello2'}; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'hello2']); + + // Should detect value change + context['a']['b'] = 'bye'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'hello2', 'bye']); + + // should cleanup after itself + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + context['a']['b'] = 'cant see me'; + watchGrp.detectChanges(); + expect(logger).toEqual(['hello', 'hello2', 'bye']); + }); - FunctionApply fn = new LoggingFunctionApply(logger); - var watch = watchGrp.watch( - new PureFunctionAST('add', fn, [parse('a.val')]), - (v, p) => logger(v) - ); + it('should reuse handlers', () { + var user1 = {'first': 'misko', 'last': 'hevery'}; + var user2 = {'first': 'misko', 'last': 'Hevery'}; - // a; a.val; b; b.val; - expect(watchGrp.fieldCost).toEqual(2); - // add - expect(watchGrp.evalCost).toEqual(1); + context['user'] = user1; - watchGrp.detectChanges(); - expect(logger).toEqual([[1], null]); - logger.clear(); + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); + var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); + var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); + expect(watchGrp.fieldCost).toEqual(3); - context['a'] = {'val': 2}; - watchGrp.detectChanges(); - expect(logger).toEqual([[2]]); - }); + watchGrp.detectChanges(); + expect(logger).toEqual([user1, 'misko', 'hevery']); + logger.clear(); + context['user'] = user2; + watchGrp.detectChanges(); + expect(logger).toEqual([user2, 'Hevery']); - it('should eval pure function', () { - context['a'] = {'val': 1}; - context['b'] = {'val': 2}; - var watch = watchGrp.watch( - new PureFunctionAST('add', - (a, b) { logger('+'); return a+b; }, - [parse('a.val'), parse('b.val')] - ), - (v, p) => logger(v) - ); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(3); - // a; a.val; b; b.val; - expect(watchGrp.fieldCost).toEqual(4); - // add - expect(watchGrp.evalCost).toEqual(1); + watchFirst.remove(); + expect(watchGrp.fieldCost).toEqual(2); - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3]); + watchLast.remove(); + expect(watchGrp.fieldCost).toEqual(0); - // extra checks should not trigger functions - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3]); + expect(() => watch.remove()).toThrow('Already deleted!'); + }); - // multiple arg changes should only trigger function once. - context['a']['val'] = 3; - context['b']['val'] = 4; + it('should eval pure FunctionApply', () { + context['a'] = {'val': 1}; - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3, '+', 7]); + FunctionApply fn = new LoggingFunctionApply(logger); + var watch = watchGrp.watch( + new PureFunctionAST('add', fn, [parse('a.val')]), + (v, p) => logger(v) + ); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + // a; a.val; b; b.val; + expect(watchGrp.fieldCost).toEqual(2); + // add + expect(watchGrp.evalCost).toEqual(1); - context['a']['val'] = 0; - context['b']['val'] = 0; + watchGrp.detectChanges(); + expect(logger).toEqual([[1], null]); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['+', 3, '+', 7]); - }); + context['a'] = {'val': 2}; + watchGrp.detectChanges(); + expect(logger).toEqual([[2]]); + }); - it('should eval chained pure function', () { - context['a'] = {'val': 1}; - context['b'] = {'val': 2}; - context['c'] = {'val': 3}; + it('should eval pure function', () { + context['a'] = {'val': 1}; + context['b'] = {'val': 2}; - var a_plus_b = new PureFunctionAST('add1', - (a, b) { logger('$a+$b'); return a + b; }, - [parse('a.val'), parse('b.val')]); + var watch = watchGrp.watch( + new PureFunctionAST('add', + (a, b) { logger('+'); return a+b; }, + [parse('a.val'), parse('b.val')] + ), + (v, p) => logger(v) + ); - var a_plus_b_plus_c = new PureFunctionAST('add2', - (b, c) { logger('$b+$c'); return b + c; }, - [a_plus_b, parse('c.val')]); + // a; a.val; b; b.val; + expect(watchGrp.fieldCost).toEqual(4); + // add + expect(watchGrp.evalCost).toEqual(1); - var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3]); - // a; a.val; b; b.val; c; c.val; - expect(watchGrp.fieldCost).toEqual(6); - // add - expect(watchGrp.evalCost).toEqual(2); + // extra checks should not trigger functions + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3]); - watchGrp.detectChanges(); - expect(logger).toEqual(['1+2', '3+3', 6]); - logger.clear(); + // multiple arg changes should only trigger function once. + context['a']['val'] = 3; + context['b']['val'] = 4; - // extra checks should not trigger functions - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3, '+', 7]); - // multiple arg changes should only trigger function once. - context['a']['val'] = 3; - context['b']['val'] = 4; - context['c']['val'] = 5; - watchGrp.detectChanges(); - expect(logger).toEqual(['3+4', '7+5', 12]); - logger.clear(); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - context['a']['val'] = 9; - watchGrp.detectChanges(); - expect(logger).toEqual(['9+4', '13+5', 18]); - logger.clear(); + context['a']['val'] = 0; + context['b']['val'] = 0; - context['c']['val'] = 9; - watchGrp.detectChanges(); - expect(logger).toEqual(['13+9', 22]); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['+', 3, '+', 7]); + }); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + it('should eval chained pure function', () { + context['a'] = {'val': 1}; + context['b'] = {'val': 2}; + context['c'] = {'val': 3}; - context['a']['val'] = 0; - context['b']['val'] = 0; + var a_plus_b = new PureFunctionAST('add1', + (a, b) { logger('$a+$b'); return a + b; }, + [parse('a.val'), parse('b.val')]); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + var a_plus_b_plus_c = new PureFunctionAST('add2', + (b, c) { logger('$b+$c'); return b + c; }, + [a_plus_b, parse('c.val')]); + var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); - it('should eval closure', () { - var obj; - obj = { - 'methodA': (arg1) { - logger('methodA($arg1) => ${obj['valA']}'); - return obj['valA']; - }, - 'valA': 'A' - }; - context['obj'] = obj; - context['arg0'] = 1; - - var watch = watchGrp.watch( - new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), - (v, p) => logger(v) - ); - - // obj, arg0; - expect(watchGrp.fieldCost).toEqual(2); - // methodA() - expect(watchGrp.evalCost).toEqual(1); + // a; a.val; b; b.val; c; c.val; + expect(watchGrp.fieldCost).toEqual(6); + // add + expect(watchGrp.evalCost).toEqual(2); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'A']); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['1+2', '3+3', 6]); + logger.clear(); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); - logger.clear(); + // extra checks should not trigger functions + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual([]); + logger.clear(); - obj['valA'] = 'B'; - context['arg0'] = 2; + // multiple arg changes should only trigger function once. + context['a']['val'] = 3; + context['b']['val'] = 4; + context['c']['val'] = 5; + watchGrp.detectChanges(); + expect(logger).toEqual(['3+4', '7+5', 12]); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(2) => B', 'B']); - logger.clear(); + context['a']['val'] = 9; + watchGrp.detectChanges(); + expect(logger).toEqual(['9+4', '13+5', 18]); + logger.clear(); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + context['c']['val'] = 9; + watchGrp.detectChanges(); + expect(logger).toEqual(['13+9', 22]); + logger.clear(); - obj['valA'] = 'C'; - context['arg0'] = 3; - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); + context['a']['val'] = 0; + context['b']['val'] = 0; - it('should eval method', () { - var obj = new MyClass(logger); - obj.valA = 'A'; - context['obj'] = obj; - context['arg0'] = 1; + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); - var watch = watchGrp.watch( - new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), - (v, p) => logger(v) - ); - // obj, arg0; - expect(watchGrp.fieldCost).toEqual(2); - // methodA() - expect(watchGrp.evalCost).toEqual(1); + it('should eval closure', () { + var obj; + obj = { + 'methodA': (arg1) { + logger('methodA($arg1) => ${obj['valA']}'); + return obj['valA']; + }, + 'valA': 'A' + }; + context['obj'] = obj; + context['arg0'] = 1; + + var watch = watchGrp.watch( + new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), + (v, p) => logger(v) + ); + + // obj, arg0; + expect(watchGrp.fieldCost).toEqual(2); + // methodA() + expect(watchGrp.evalCost).toEqual(1); + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'A']); + logger.clear(); + + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); + logger.clear(); + + obj['valA'] = 'B'; + context['arg0'] = 2; + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(2) => B', 'B']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'A']); - logger.clear(); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); - logger.clear(); + obj['valA'] = 'C'; + context['arg0'] = 3; - obj.valA = 'B'; - context['arg0'] = 2; + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(2) => B', 'B']); - logger.clear(); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + it('should eval method', () { + var obj = new MyClass(logger); + obj.valA = 'A'; + context['obj'] = obj; + context['arg0'] = 1; - obj.valA = 'C'; - context['arg0'] = 3; + var watch = watchGrp.watch( + new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), + (v, p) => logger(v) + ); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + // obj, arg0; + expect(watchGrp.fieldCost).toEqual(2); + // methodA() + expect(watchGrp.evalCost).toEqual(1); - it('should eval method chain', () { - var obj1 = new MyClass(logger); - var obj2 = new MyClass(logger); - obj1.valA = obj2; - obj2.valA = 'A'; - context['obj'] = obj1; - context['arg0'] = 0; - context['arg1'] = 1; - - // obj.methodA(arg0) - var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); - ast = new MethodAST(ast, 'methodA', [parse('arg1')]); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); - - // obj, arg0, arg1; - expect(watchGrp.fieldCost).toEqual(3); - // methodA(), methodA() - expect(watchGrp.evalCost).toEqual(2); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'A']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']); - logger.clear(); + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(1) => A', 'methodA(1) => A']); + logger.clear(); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', - 'methodA(0) => MyClass', 'methodA(1) => A']); - logger.clear(); + obj.valA = 'B'; + context['arg0'] = 2; - obj2.valA = 'B'; - context['arg0'] = 10; - context['arg1'] = 11; + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(2) => B', 'B']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B']); - logger.clear(); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); + obj.valA = 'C'; + context['arg0'] = 3; - obj2.valA = 'C'; - context['arg0'] = 20; - context['arg1'] = 21; + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); - watchGrp.detectChanges(); - expect(logger).toEqual([]); - }); + it('should eval method chain', () { + var obj1 = new MyClass(logger); + var obj2 = new MyClass(logger); + obj1.valA = obj2; + obj2.valA = 'A'; + context['obj'] = obj1; + context['arg0'] = 0; + context['arg1'] = 1; + + // obj.methodA(arg0) + var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); + ast = new MethodAST(ast, 'methodA', [parse('arg1')]); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + + // obj, arg0, arg1; + expect(watchGrp.fieldCost).toEqual(3); + // methodA(), methodA() + expect(watchGrp.evalCost).toEqual(2); + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', 'A']); + logger.clear(); + + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(0) => MyClass', 'methodA(1) => A', + 'methodA(0) => MyClass', 'methodA(1) => A']); + logger.clear(); + + obj2.valA = 'B'; + context['arg0'] = 10; + context['arg1'] = 11; + + watchGrp.detectChanges(); + expect(logger).toEqual(['methodA(10) => MyClass', 'methodA(11) => B', 'B']); + logger.clear(); - it('should not return null when evaling method first time', () { - context['text'] ='abc'; - var ast = new MethodAST(parse('text'), 'toUpperCase', []); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); - watchGrp.detectChanges(); - expect(logger).toEqual(['ABC']); - }); + obj2.valA = 'C'; + context['arg0'] = 20; + context['arg1'] = 21; - it('should not eval a function if registered during reaction', () { - context['text'] ='abc'; - var ast = new MethodAST(parse('text'), 'toLowerCase', []); - var watch = watchGrp.watch(ast, (v, p) { - var ast = new MethodAST(parse('text'), 'toUpperCase', []); - watchGrp.watch(ast, (v, p) { - logger(v); - }); + watchGrp.detectChanges(); + expect(logger).toEqual([]); }); - watchGrp.detectChanges(); - watchGrp.detectChanges(); - expect(logger).toEqual(['ABC']); - }); - + it('should not return null when evaling method first time', () { + context['text'] ='abc'; + var ast = new MethodAST(parse('text'), 'toUpperCase', []); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); - it('should eval function eagerly when registered during reaction', () { - var fn = (arg) { logger('fn($arg)'); return arg; }; - context['obj'] = {'fn': fn}; - context['arg1'] = 'OUT'; - context['arg2'] = 'IN'; - var ast = new MethodAST(parse('obj'), 'fn', [parse('arg1')]); - var watch = watchGrp.watch(ast, (v, p) { - var ast = new MethodAST(parse('obj'), 'fn', [parse('arg2')]); - watchGrp.watch(ast, (v, p) { - logger('reaction: $v'); - }); + watchGrp.detectChanges(); + expect(logger).toEqual(['ABC']); }); - expect(logger).toEqual([]); - watchGrp.detectChanges(); - expect(logger).toEqual(['fn(OUT)', 'fn(IN)', 'reaction: IN']); - logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['fn(OUT)', 'fn(IN)']); - }); - + it('should not eval a function if registered during reaction', () { + context['text'] ='abc'; + var ast = new MethodAST(parse('text'), 'toLowerCase', []); + var watch = watchGrp.watch(ast, (v, p) { + var ast = new MethodAST(parse('text'), 'toUpperCase', []); + watchGrp.watch(ast, (v, p) { + logger(v); + }); + }); - it('should read connstant', () { - // should fire on initial adding - expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); - expect(watch.expression).toEqual('123'); - expect(watchGrp.fieldCost).toEqual(0); - watchGrp.detectChanges(); - expect(logger).toEqual([123]); + watchGrp.detectChanges(); + watchGrp.detectChanges(); + expect(logger).toEqual(['ABC']); + }); - // make sore no new changes are logged on extra detectChanges - watchGrp.detectChanges(); - expect(logger).toEqual([123]); - }); - it('should wrap iterable in ObservableList', () { - context['list'] = []; - var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => logger(v)); + it('should eval function eagerly when registered during reaction', () { + var fn = (arg) { logger('fn($arg)'); return arg; }; + context['obj'] = {'fn': fn}; + context['arg1'] = 'OUT'; + context['arg2'] = 'IN'; + var ast = new MethodAST(parse('obj'), 'fn', [parse('arg1')]); + var watch = watchGrp.watch(ast, (v, p) { + var ast = new MethodAST(parse('obj'), 'fn', [parse('arg2')]); + watchGrp.watch(ast, (v, p) { + logger('reaction: $v'); + }); + }); - expect(watchGrp.fieldCost).toEqual(1); - expect(watchGrp.collectionCost).toEqual(1); - expect(watchGrp.evalCost).toEqual(0); + expect(logger).toEqual([]); + watchGrp.detectChanges(); + expect(logger).toEqual(['fn(OUT)', 'fn(IN)', 'reaction: IN']); + logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['fn(OUT)', 'fn(IN)']); + }); - watchGrp.detectChanges(); - expect(logger.length).toEqual(1); - expect(logger[0], toEqualCollectionRecord( - collection: [], - additions: [], - moves: [], - removals: [])); - logger.clear(); - context['list'] = [1]; - watchGrp.detectChanges(); - expect(logger.length).toEqual(1); - expect(logger[0], toEqualCollectionRecord( - collection: ['1[null -> 0]'], - additions: ['1[null -> 0]'], - moves: [], - removals: [])); - logger.clear(); + it('should read connstant', () { + // should fire on initial adding + expect(watchGrp.fieldCost).toEqual(0); + var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); + expect(watch.expression).toEqual('123'); + expect(watchGrp.fieldCost).toEqual(0); + watchGrp.detectChanges(); + expect(logger).toEqual([123]); - watch.remove(); - expect(watchGrp.fieldCost).toEqual(0); - expect(watchGrp.collectionCost).toEqual(0); - expect(watchGrp.evalCost).toEqual(0); - }); + // make sore no new changes are logged on extra detectChanges + watchGrp.detectChanges(); + expect(logger).toEqual([123]); + }); - it('should watch literal arrays made of expressions', () { - context['a'] = 1; - var ast = new CollectionAST( - new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) - ); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); - watchGrp.detectChanges(); - expect(logger[0], toEqualCollectionRecord( - collection: ['1[null -> 0]'], - additions: ['1[null -> 0]'], - moves: [], - removals: [])); - logger.clear(); + it('should wrap iterable in ObservableList', () { + context['list'] = []; + var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => logger(v)); + + expect(watchGrp.fieldCost).toEqual(1); + expect(watchGrp.collectionCost).toEqual(1); + expect(watchGrp.evalCost).toEqual(0); + + watchGrp.detectChanges(); + expect(logger.length).toEqual(1); + expect(logger[0], toEqualCollectionRecord( + collection: [], + additions: [], + moves: [], + removals: [])); + logger.clear(); + + context['list'] = [1]; + watchGrp.detectChanges(); + expect(logger.length).toEqual(1); + expect(logger[0], toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + logger.clear(); - context['a'] = 2; - watchGrp.detectChanges(); - expect(logger[0], toEqualCollectionRecord( - collection: ['2[null -> 0]'], - additions: ['2[null -> 0]'], - moves: [], - removals: ['1[0 -> null]'])); - logger.clear(); - }); + watch.remove(); + expect(watchGrp.fieldCost).toEqual(0); + expect(watchGrp.collectionCost).toEqual(0); + expect(watchGrp.evalCost).toEqual(0); + }); - it('should watch pure function whose result goes to pure function', () { - context['a'] = 1; - var ast = new PureFunctionAST( - '-', - (v) => -v, - [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] - ); - var watch = watchGrp.watch(ast, (v, p) => logger(v)); - - expect(watchGrp.detectChanges()).not.toBe(null); - expect(logger).toEqual([-2]); - logger.clear(); + it('should watch literal arrays made of expressions', () { + context['a'] = 1; + var ast = new CollectionAST( + new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) + ); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger[0], toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + logger.clear(); + + context['a'] = 2; + watchGrp.detectChanges(); + expect(logger[0], toEqualCollectionRecord( + collection: ['2[null -> 0]'], + additions: ['2[null -> 0]'], + moves: [], + removals: ['1[0 -> null]'])); + logger.clear(); + }); - context['a'] = 2; - expect(watchGrp.detectChanges()).not.toBe(null); - expect(logger).toEqual([-3]); + it('should watch pure function whose result goes to pure function', () { + context['a'] = 1; + var ast = new PureFunctionAST( + '-', + (v) => -v, + [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] + ); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + + expect(watchGrp.detectChanges()).not.toBe(null); + expect(logger).toEqual([-2]); + logger.clear(); + + context['a'] = 2; + expect(watchGrp.detectChanges()).not.toBe(null); + expect(logger).toEqual([-3]); + }); }); - }); - - describe('child group', () { - it('should remove all field watches in group and group\'s children', () { - watchGrp.watch(parse('a'), (v, p) => logger('0a')); - var child1a = watchGrp.newGroup(new PrototypeMap(context)); - var child1b = watchGrp.newGroup(new PrototypeMap(context)); - var child2 = child1a.newGroup(new PrototypeMap(context)); - child1a.watch(parse('a'), (v, p) => logger('1a')); - child1b.watch(parse('a'), (v, p) => logger('1b')); - watchGrp.watch(parse('a'), (v, p) => logger('0A')); - child1a.watch(parse('a'), (v, p) => logger('1A')); - child2.watch(parse('a'), (v, p) => logger('2A')); - - // flush initial reaction functions - expect(watchGrp.detectChanges()).toEqual(6); - // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); - expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go by registration order - expect(watchGrp.fieldCost).toEqual(1); - expect(watchGrp.totalFieldCost).toEqual(4); - logger.clear(); - context['a'] = 1; - expect(watchGrp.detectChanges()).toEqual(6); - expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go by group order - logger.clear(); - - context['a'] = 2; - child1a.remove(); // should also remove child2 - expect(watchGrp.detectChanges()).toEqual(3); - expect(logger).toEqual(['0a', '0A', '1b']); - expect(watchGrp.fieldCost).toEqual(1); - expect(watchGrp.totalFieldCost).toEqual(2); - }); + describe('child group', () { + it('should remove all field watches in group and group\'s children', () { + watchGrp.watch(parse('a'), (v, p) => logger('0a')); + var child1a = watchGrp.newGroup(new PrototypeMap(context)); + var child1b = watchGrp.newGroup(new PrototypeMap(context)); + var child2 = child1a.newGroup(new PrototypeMap(context)); + child1a.watch(parse('a'), (v, p) => logger('1a')); + child1b.watch(parse('a'), (v, p) => logger('1b')); + watchGrp.watch(parse('a'), (v, p) => logger('0A')); + child1a.watch(parse('a'), (v, p) => logger('1A')); + child2.watch(parse('a'), (v, p) => logger('2A')); + + // flush initial reaction functions + expect(watchGrp.detectChanges()).toEqual(6); + // expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); + expect(logger).toEqual(['0a', '1a', '1b', '0A', '1A', '2A']); // we go by registration order + expect(watchGrp.fieldCost).toEqual(1); + expect(watchGrp.totalFieldCost).toEqual(4); + logger.clear(); + + context['a'] = 1; + expect(watchGrp.detectChanges()).toEqual(6); + expect(logger).toEqual(['0a', '0A', '1a', '1A', '2A', '1b']); // we go by group order + logger.clear(); + + context['a'] = 2; + child1a.remove(); // should also remove child2 + expect(watchGrp.detectChanges()).toEqual(3); + expect(logger).toEqual(['0a', '0A', '1b']); + expect(watchGrp.fieldCost).toEqual(1); + expect(watchGrp.totalFieldCost).toEqual(2); + }); - it('should remove all method watches in group and group\'s children', () { - context['my'] = new MyClass(logger); - AST countMethod = new MethodAST(parse('my'), 'count', []); - watchGrp.watch(countMethod, (v, p) => logger('0a')); - expectOrder(['0a']); - - var child1a = watchGrp.newGroup(new PrototypeMap(context)); - var child1b = watchGrp.newGroup(new PrototypeMap(context)); - var child2 = child1a.newGroup(new PrototypeMap(context)); - child1a.watch(countMethod, (v, p) => logger('1a')); - expectOrder(['0a', '1a']); - child1b.watch(countMethod, (v, p) => logger('1b')); - expectOrder(['0a', '1a', '1b']); - watchGrp.watch(countMethod, (v, p) => logger('0A')); - expectOrder(['0a', '0A', '1a', '1b']); - child1a.watch(countMethod, (v, p) => logger('1A')); - expectOrder(['0a', '0A', '1a', '1A', '1b']); - child2.watch(countMethod, (v, p) => logger('2A')); - expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); - - // flush initial reaction functions - expect(watchGrp.detectChanges()).toEqual(6); - expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); - - child1a.remove(); // should also remove child2 - expect(watchGrp.detectChanges()).toEqual(3); - expectOrder(['0a', '0A', '1b']); - }); + it('should remove all method watches in group and group\'s children', () { + context['my'] = new MyClass(logger); + AST countMethod = new MethodAST(parse('my'), 'count', []); + watchGrp.watch(countMethod, (v, p) => logger('0a')); + expectOrder(['0a']); + + var child1a = watchGrp.newGroup(new PrototypeMap(context)); + var child1b = watchGrp.newGroup(new PrototypeMap(context)); + var child2 = child1a.newGroup(new PrototypeMap(context)); + child1a.watch(countMethod, (v, p) => logger('1a')); + expectOrder(['0a', '1a']); + child1b.watch(countMethod, (v, p) => logger('1b')); + expectOrder(['0a', '1a', '1b']); + watchGrp.watch(countMethod, (v, p) => logger('0A')); + expectOrder(['0a', '0A', '1a', '1b']); + child1a.watch(countMethod, (v, p) => logger('1A')); + expectOrder(['0a', '0A', '1a', '1A', '1b']); + child2.watch(countMethod, (v, p) => logger('2A')); + expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); + + // flush initial reaction functions + expect(watchGrp.detectChanges()).toEqual(6); + expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); + + child1a.remove(); // should also remove child2 + expect(watchGrp.detectChanges()).toEqual(3); + expectOrder(['0a', '0A', '1b']); + }); - it('should add watches within its own group', () { - context['my'] = new MyClass(logger); - AST countMethod = new MethodAST(parse('my'), 'count', []); - var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); - var child = watchGrp.newGroup(new PrototypeMap(context)); - var cb = child.watch(countMethod, (v, p) => logger('b')); + it('should add watches within its own group', () { + context['my'] = new MyClass(logger); + AST countMethod = new MethodAST(parse('my'), 'count', []); + var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); + var child = watchGrp.newGroup(new PrototypeMap(context)); + var cb = child.watch(countMethod, (v, p) => logger('b')); - expectOrder(['a', 'b']); - expectOrder(['a', 'b']); + expectOrder(['a', 'b']); + expectOrder(['a', 'b']); - ra.remove(); - expectOrder(['b']); + ra.remove(); + expectOrder(['b']); - cb.remove(); - expectOrder([]); + cb.remove(); + expectOrder([]); - // TODO: add them back in wrong order, assert events in right order - cb = child.watch(countMethod, (v, p) => logger('b')); - ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; - expectOrder(['a', 'b']); - }); + // TODO: add them back in wrong order, assert events in right order + cb = child.watch(countMethod, (v, p) => logger('b')); + ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; + expectOrder(['a', 'b']); + }); - it('should not call reaction function on removed group', () { - var log = []; - context['name'] = 'misko'; - var child = watchGrp.newGroup(context); - watchGrp.watch(parse('name'), (v, _) { - log.add('root $v'); - if (v == 'destroy') { - child.remove(); - } + it('should not call reaction function on removed group', () { + var log = []; + context['name'] = 'misko'; + var child = watchGrp.newGroup(context); + watchGrp.watch(parse('name'), (v, _) { + log.add('root $v'); + if (v == 'destroy') { + child.remove(); + } + }); + child.watch(parse('name'), (v, _) => log.add('child $v')); + watchGrp.detectChanges(); + expect(log).toEqual(['root misko', 'child misko']); + log.clear(); + + context['name'] = 'destroy'; + watchGrp.detectChanges(); + expect(log).toEqual(['root destroy']); }); - child.watch(parse('name'), (v, _) => log.add('child $v')); - watchGrp.detectChanges(); - expect(log).toEqual(['root misko', 'child misko']); - log.clear(); - context['name'] = 'destroy'; - watchGrp.detectChanges(); - expect(log).toEqual(['root destroy']); - }); + it('should watch children', () { + var childContext = new PrototypeMap(context); + context['a'] = 'OK'; + context['b'] = 'BAD'; + childContext['b'] = 'OK'; + watchGrp.watch(parse('a'), (v, p) => logger(v)); + watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); - it('should watch children', () { - var childContext = new PrototypeMap(context); - context['a'] = 'OK'; - context['b'] = 'BAD'; - childContext['b'] = 'OK'; - watchGrp.watch(parse('a'), (v, p) => logger(v)); - watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['OK', 'OK']); + logger.clear(); - watchGrp.detectChanges(); - expect(logger).toEqual(['OK', 'OK']); - logger.clear(); + context['a'] = 'A'; + childContext['b'] = 'B'; - context['a'] = 'A'; - childContext['b'] = 'B'; - - watchGrp.detectChanges(); - expect(logger).toEqual(['A', 'B']); - logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual(['A', 'B']); + logger.clear(); + }); }); }); - - }); + } + they(new DynamicFieldGetterFactory(), 'dynamic'); + they(staticFieldGetterFactory, 'static'); } class MyClass { diff --git a/test/core/core_directive_spec.dart b/test/core/core_directive_spec.dart index fc9dd5a9c..1ab08c9d2 100644 --- a/test/core/core_directive_spec.dart +++ b/test/core/core_directive_spec.dart @@ -1,6 +1,7 @@ library core_directive_spec; import '../_specs.dart'; +import 'package:angular/angular_dynamic.dart'; void main() { describe('DirectiveMap', () { @@ -43,15 +44,14 @@ void main() { baseModule = new Module() ..type(DirectiveMap) ..type(DirectiveSelectorFactory) - ..type(MetadataExtractor) - ..type(FieldMetadataExtractor); + ..type(MetadataExtractor); }); it('should throw when annotation is for existing mapping', () { var module = new Module() ..type(Bad1Component); - var injector = new DynamicInjector(modules: [baseModule, module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() { injector.get(DirectiveMap); }).toThrow('Mapping for attribute foo is already defined (while ' @@ -62,7 +62,7 @@ void main() { var module = new Module() ..type(Bad2Component); - var injector = new DynamicInjector(modules: [baseModule, module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() { injector.get(DirectiveMap); }).toThrow('Attribute annotation for foo is defined more than once ' diff --git a/test/core/registry_spec.dart b/test/core/registry_spec.dart index 1c9c6070b..2facdefe1 100644 --- a/test/core/registry_spec.dart +++ b/test/core/registry_spec.dart @@ -1,17 +1,17 @@ library registry_spec; import '../_specs.dart'; +import 'package:angular/angular_dynamic.dart'; main() { describe('RegistryMap', () { it('should allow for multiple registry keys to be added', () { var module = new Module() ..type(MyMap) - ..type(MetadataExtractor) ..type(A1) ..type(A2); - var injector = new DynamicInjector(modules: [module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() { injector.get(MyMap); }).not.toThrow(); @@ -20,10 +20,9 @@ main() { it('should iterate over all types', () { var module = new Module() ..type(MyMap) - ..type(MetadataExtractor) ..type(A1); - var injector = new DynamicInjector(modules: [module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); var keys = []; var types = []; var map = injector.get(MyMap); @@ -35,10 +34,9 @@ main() { it('should safely ignore typedefs', () { var module = new Module() ..type(MyMap) - ..type(MetadataExtractor) ..value(MyTypedef, (String _) => null); - var injector = new DynamicInjector(modules: [module]); + var injector = new NgDynamicApp().addModule(module).createInjector(); expect(() => injector.get(MyMap), isNot(throws)); }); }); diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index c2d2259bb..f5c841dd9 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -11,7 +11,6 @@ void main() { beforeEach(module((Module module) { Map context = {}; module - ..value(GetterCache, new GetterCache({})) ..type(ChangeDetector, implementedBy: DirtyCheckingChangeDetector) ..value(Object, context) ..value(Map, context) diff --git a/test/core_dom/block_spec.dart b/test/core_dom/block_spec.dart index fefa1f299..f6c7bc0ee 100644 --- a/test/core_dom/block_spec.dart +++ b/test/core_dom/block_spec.dart @@ -1,6 +1,7 @@ library block_spec; import '../_specs.dart'; +import 'package:angular/angular_dynamic.dart'; class Log { List log = []; @@ -193,8 +194,9 @@ main() { ..type(AFilter) ..type(ADirective); - Injector rootInjector = - new DynamicInjector(modules: [new AngularModule(), rootModule]); + Injector rootInjector = new NgDynamicApp() + .addModule(rootModule) + .createInjector(); Log log = rootInjector.get(Log); Scope rootScope = rootInjector.get(Scope); diff --git a/test/introspection_spec.dart b/test/introspection_spec.dart index d30c9b139..8fe9cf206 100644 --- a/test/introspection_spec.dart +++ b/test/introspection_spec.dart @@ -2,6 +2,7 @@ library introspection_spec; import '_specs.dart'; import 'dart:js' as js; +import 'package:angular/angular_dynamic.dart'; void main() { describe('introspection', () { @@ -43,7 +44,7 @@ void main() { var elt = $('
')[0]; // Make it possible to find the element from JS document.body.append(elt); - ngBootstrap(element: elt); + (new NgDynamicApp()..element = elt).run(); expect(js.context['ngProbe']).toBeDefined(); expect(js.context['ngScope']).toBeDefined(); diff --git a/test/jasmine_syntax.dart b/test/jasmine_syntax.dart index 13def2068..8e1bfa48d 100644 --- a/test/jasmine_syntax.dart +++ b/test/jasmine_syntax.dart @@ -7,9 +7,9 @@ Function _wrapFn; _maybeWrapFn(fn) => () { if (_wrapFn != null) { - _wrapFn(fn)(); + return _wrapFn(fn)(); } else { - fn(); + return fn(); } }; diff --git a/test/routing/routing_spec.dart b/test/routing/routing_spec.dart index 933cc1ca1..2a82adf7e 100644 --- a/test/routing/routing_spec.dart +++ b/test/routing/routing_spec.dart @@ -2,6 +2,7 @@ library routing_spec; import '../_specs.dart'; import 'package:angular/mock/module.dart'; +import 'package:angular/angular_dynamic.dart'; import 'dart:async'; main() { @@ -44,10 +45,10 @@ main() { }); initRouter(initializer) { - var module = new Module() - ..value(RouteInitializerFn, initializer); - var injector = new DynamicInjector( - modules: [new AngularModule(), new AngularMockModule(), module]); + var injector = new NgDynamicApp() + .addModule(new AngularMockModule()) + .addModule(new Module()..value(RouteInitializerFn, initializer)) + .createInjector(); injector.get(NgRoutingHelper); // force routing initialization router = injector.get(Router); _ = injector.get(TestBed); diff --git a/test/tools/transformer/all.dart b/test/tools/transformer/all.dart new file mode 100644 index 000000000..fa81732f4 --- /dev/null +++ b/test/tools/transformer/all.dart @@ -0,0 +1,11 @@ +library all_transformer_tests; + +import 'expression_generator_spec.dart' as expression_generator_spec; +import 'metadata_generator_spec.dart' as metadata_generator_spec; +import 'static_angular_generator_spec.dart' as static_angular_generator_spec; + +main() { + expression_generator_spec.main(); + metadata_generator_spec.main(); + static_angular_generator_spec.main(); +} diff --git a/test/tools/transformer/expression_generator_spec.dart b/test/tools/transformer/expression_generator_spec.dart new file mode 100644 index 000000000..d36a15426 --- /dev/null +++ b/test/tools/transformer/expression_generator_spec.dart @@ -0,0 +1,68 @@ +library angular.test.tools.transformer.expression_extractor_spec; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; +import '../../jasmine_syntax.dart'; + +main() { + describe('ExpressionGenerator', () { + var htmlFiles = []; + var options = new TransformOptions( + dartEntries: ['web/main.dart'], + htmlFiles: htmlFiles, + sdkDirectory: dartSdkDirectory); + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new ExpressionGenerator(options, resolvers)] + ]; + + it('should extract expressions', () { + htmlFiles.add('web/index.html'); + return tests.applyTransformers(phases, + inputs: { + 'a|web/main.dart': ''' +library foo; +''', + 'a|web/index.html': ''' +
{{some.getter}}
+''' + }, + results: { + 'a|web/main_static_expressions.dart': ''' +$header +final Map getters = { + r"some": (o) => o.some, + r"getter": (o) => o.getter +}; +final Map setters = { + r"some": (o, v) => o.some = v, + r"getter": (o, v) => o.getter = v +}; +final List> functions = []; +''' + }).whenComplete(() { + htmlFiles.clear(); + }); + }); + }); +} + +const String header = ''' +library a.web.main.generated_expressions; + +import 'package:angular/angular.dart'; +import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; + +Module get expressionModule => new Module() + ..value(ClosureMap, new StaticClosureMap()); + +class StaticClosureMap extends ClosureMap { + Getter lookupGetter(String name) => getters[name]; + Setter lookupSetter(String name) => setters[name]; + lookupFunction(String name, int arity) + => (arity < functions.length) ? functions[arity][name] : null; +} +'''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart new file mode 100644 index 000000000..61e9ebe07 --- /dev/null +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -0,0 +1,506 @@ +library angular.test.tools.transformer.metadata_generator_spec; + +import 'dart:async'; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('MetadataGenerator', () { + var options = new TransformOptions( + dartEntries: ['web/main.dart'], + sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new MetadataGenerator(options, resolvers)] + ]; + + it('should extract member metadata', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(selector: r'[*=/{{.*}}/]') + @proxy + class Engine { + @NgOneWay('another-expression') + String anotherExpression; + + @NgCallback('callback') + set callback(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\', map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'callback\': \'&callback\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'' + '})', + 'proxy', + ] + }); + }); + + it('should warn on multiple annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + @NgOneWay('another-expression') + set callback(Function) {} + } + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(main.dart 3 18)']); + }); + + it('should warn on duplicated annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: {'another-expression': '=>anotherExpression'}) + class Engine { + @NgOneWay('another-expression') + set anotherExpression(Function) {} + } + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {' + '\'another-expression\': \'=>anotherExpression\'})', + ] + }, + messages: ['warning: Directive @NgOneWay(\'another-expression\') ' + 'already contains an entry for \'another-expression\' ' + '(main.dart 2 16)' + ]); + }); + + it('should merge member annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: {'another-expression': '=>anotherExpression'}) + class Engine { + set anotherExpression(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'})', + ] + }); + }); + + it('should warn on multiple annotations (across getter/setter)', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + set callback(Function) {} + + @NgOneWay('another-expression') + get callback() {} + } + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(main.dart 3 18)']); + }); + + it('should extract map arguments', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) + class Engine {} + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {\'ng-value\': \'&ngValue\', \'key\': \'value\'})', + ] + }); + }); + + it('should extract list arguments', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(publishTypes: const [TextChangeListener]) + class Engine {} + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(publishTypes: const [import_1.TextChangeListener,])', + ] + }); + }); + + it('should extract primitive literals', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgOneWay(true) + @NgOneWay(1.0) + @NgOneWay(1) + @NgOneWay(null) + class Engine {} + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgOneWay(true)', + 'const import_1.NgOneWay(1.0)', + 'const import_1.NgOneWay(1)', + 'const import_1.NgOneWay(null)', + ] + }); + }); + + it('should skip and warn on unserializable annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @Foo + class Engine {} + + @NgDirective(publishTypes: const [Foo]) + class Car {} + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'null', + ], + 'import_0.Car': [ + 'null', + ] + }, + messages: [ + 'warning: Unable to serialize annotation @Foo. (main.dart 2 16)', + 'warning: Unable to serialize annotation ' + '@NgDirective(publishTypes: const [Foo]). (main.dart 5 16)', + ]); + }); + + it('should extract types across libs', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + import 'package:a/b.dart'; + + @NgDirective(publishTypes: const [Car]) + class Engine {} + ''', + 'a|lib/b.dart': ''' + class Car {} + ''', + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + 'import \'package:a/b.dart\' as import_2;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(publishTypes: const [import_2.Car,])', + ] + }); + }); + + it('should not gather non-member annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + Engine() { + @NgDirective() + print('something'); + } + } + ''', + }); + }); + + it('properly escapes strings', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': r''' + import 'package:angular/angular.dart'; + + @NgOneWay('foo\' \\') + class Engine { + } + ''', + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + r'''const import_1.NgOneWay('foo\' \\')''', + ] + }); + }); + + it('should reference static and global properties', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(visibility: NgDirective.CHILDREN_VISIBILITY) + @NgDirective(visibility: CONST_VALUE) + class Engine {} + + const int CONST_VALUE = 2; + ''', + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + '''const import_1.NgDirective(visibility: import_1.NgDirective.CHILDREN_VISIBILITY)''', + '''const import_1.NgDirective(visibility: import_0.CONST_VALUE)''', + ] + }); + }); + + it('should not extract private annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @_Foo() + @_foo + class Engine { + } + + class _Foo { + const _Foo(); + } + const _Foo _foo = const _Foo(); + ''', + }, + messages: [ + 'warning: Annotation @_Foo() is not public. (main.dart 2 16)', + 'warning: Annotation @_foo is not public. (main.dart 2 16)', + ]); + }); + + it('supports named constructors', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @Foo.bar() + @Foo._private() + class Engine { + } + + class Foo { + const Foo.bar(); + const Foo._private(); + } + ''', + }, + imports: [ + 'import \'main.dart\' as import_0;', + ], + classes: { + 'import_0.Engine': [ + '''const import_0.Foo.bar()''', + ] + }, + messages: [ + 'warning: Annotation @Foo._private() is not public. ' + '(main.dart 2 16)', + ]); + }); + }); +} + +Future generates(List> phases, + {Map inputs, Iterable imports: const [], + Map classes: const {}, + Iterable messages: const []}) { + + var buffer = new StringBuffer(); + buffer.write('$header\n'); + for (var i in imports) { + buffer.write('$i\n'); + } + buffer.write('$boilerPlate\n'); + for (var className in classes.keys) { + buffer.write(' $className: const [\n'); + for (var annotation in classes[className]) { + buffer.write(' $annotation,\n'); + } + buffer.write(' ],\n'); + } + + buffer.write('$footer\n'); + + return tests.applyTransformers(phases, + inputs: inputs, + results: { + 'a|web/main_static_metadata.dart': buffer.toString() + }, + messages: messages); +} + +const String header = ''' +library a.web.main.generated_metadata; + +import 'package:angular/angular.dart' show MetadataExtractor; +import 'package:di/di.dart' show Module; +'''; + +const String boilerPlate = ''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = typeAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +final Map typeAnnotations = {'''; + +const String footer = ''' +};'''; + + +const String libAngular = ''' +library angular.core; + +class NgAnnotation { + NgAnnotation({map: const {}}); +} + +class NgDirective extends NgAnnotation { + const NgDirective({selector, publishTypes, map, visibility}) : super(map: map); + + static const int CHILDREN_VISIBILITY = 1; +} + +class NgOneWay { + const NgOneWay(arg); +} + +class NgTwoWay { + const NgTwoWay(arg); +} + +class NgCallback { + const NgCallback(arg); +} + +class NgAttr { + const NgAttr(); +} +class NgOneWayOneTime { + const NgOneWayOneTime(arg); +} + +class TextChangeListener {} +'''; diff --git a/test/tools/transformer/static_angular_generator_spec.dart b/test/tools/transformer/static_angular_generator_spec.dart new file mode 100644 index 000000000..c9ece2c3a --- /dev/null +++ b/test/tools/transformer/static_angular_generator_spec.dart @@ -0,0 +1,111 @@ +library angular.test.tools.transformer.metadata_generator_spec; + +import 'dart:async'; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/static_angular_generator.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('StaticAngularGenerator', () { + var options = new TransformOptions( + dartEntries: ['web/main.dart'], + sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new StaticAngularGenerator(options, resolvers)] + ]; + + it('should modify NgDynamicApp', () { + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/angular_dynamic.dart': libAngularDynamic, + 'di|lib/di.dart': libDI, + 'a|web/main.dart': ''' +import 'package:angular/angular_dynamic.dart'; +import 'package:di/di.dart' show Module; + +class MyModule extends Module {} + +main() { + var app = new NgDynamicApp() + .addModule(new MyModule()) + .run(); +} +''' + }, + results: { + 'a|web/main.dart': ''' +import 'package:angular/angular_static.dart'; +import 'package:di/di.dart' show Module; +import 'main_static_expressions.dart' as generated_static_expressions; +import 'main_static_metadata.dart' as generated_static_metadata; +import 'main_static_injector.dart' as generated_static_injector; + +class MyModule extends Module {} + +main() { + var app = new NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + + it('handles prefixed app imports', () { + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/angular_dynamic.dart': libAngularDynamic, + 'di|lib/di.dart': libDI, + 'a|web/main.dart': ''' +import 'package:angular/angular_dynamic.dart' as ng; +import 'package:di/di.dart' show Module; + +class MyModule extends Module {} + +main() { + var app = new ng.NgDynamicApp() + .addModule(new MyModule()) + .run(); +} +''' + }, + results: { + 'a|web/main.dart': ''' +import 'package:angular/angular_static.dart' as ng; +import 'package:di/di.dart' show Module; +import 'main_static_expressions.dart' as generated_static_expressions; +import 'main_static_metadata.dart' as generated_static_metadata; +import 'main_static_injector.dart' as generated_static_injector; + +class MyModule extends Module {} + +main() { + var app = new ng.NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + }); +} + + + +const String libAngularDynamic = ''' +library angular.dynamic + +class NgDynamicApp {}; +'''; + +const String libDI = ''' +class Module {} +''';