diff --git a/gulpfile.js b/gulpfile.js index 467172529c..314d0f572e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -434,7 +434,7 @@ gulp.task('test.js', function(done) { gulp.task('test.dart', function(done) { runSequence('versions.dart', 'test.transpiler.unittest', 'test.unit.dart/ci', - sequenceComplete(done)); + 'test.dart.angular2_testing/ci', sequenceComplete(done)); }); gulp.task('versions.dart', function() { dartSdk.logVersion(DART_SDK); }); @@ -787,6 +787,27 @@ gulp.task('test.server.dart', runServerDartTests(gulp, gulpPlugins, {dest: 'dist gulp.task('test.transpiler.unittest', function(done) { runJasmineTests(['tools/transpiler/unittest/**/*.js'], done); }); +// At the moment, dart test requires dartium to be an executable on the path. +// Make a temporary directory and symlink dartium from there (just for this command) +// so that it can run. +var dartiumTmpdir = path.join(os.tmpdir(), 'dartium' + new Date().getTime().toString()); +gulp.task('test.dart.angular2_testing/ci', ['!pubget.angular2_testing.dart'], function(done) { + runSequence('test.dart.angular2_testing_symlink', 'test.dart.angular2_testing', + sequenceComplete(done)); +}); + +gulp.task( + 'test.dart.angular2_testing_symlink', + shell.task(['mkdir ' + dartiumTmpdir, 'ln -s $DARTIUM_BIN ' + dartiumTmpdir + '/dartium'])); + +gulp.task('test.dart.angular2_testing', + shell.task(['PATH=$PATH:' + dartiumTmpdir + ' pub run test -p dartium'], + {'cwd': 'modules_dart/angular2_testing'})); + +gulp.task( + '!pubget.angular2_testing.dart', + pubget.dir(gulp, gulpPlugins, {dir: 'modules_dart/angular2_testing', command: DART_SDK.PUB})); + // ----------------- // Pre-test checks diff --git a/modules/angular2/src/mock/mock_location_strategy.ts b/modules/angular2/src/mock/mock_location_strategy.ts index fd7ad2877e..1c6b48d639 100644 --- a/modules/angular2/src/mock/mock_location_strategy.ts +++ b/modules/angular2/src/mock/mock_location_strategy.ts @@ -37,8 +37,8 @@ export class MockLocationStrategy extends LocationStrategy { var url = path + (query.length > 0 ? ('?' + query) : ''); this.internalPath = url; - var external = this.prepareExternalUrl(url); - this.urlChanges.push(external); + var externalUrl = this.prepareExternalUrl(url); + this.urlChanges.push(externalUrl); } onPopState(fn: (value: any) => void): void { ObservableWrapper.subscribe(this._subject, fn); } diff --git a/modules_dart/angular2_testing/README.md b/modules_dart/angular2_testing/README.md new file mode 100644 index 0000000000..b6f4ea1965 --- /dev/null +++ b/modules_dart/angular2_testing/README.md @@ -0,0 +1,46 @@ +Contains helpers to run unit tests for angular2 components and injectables, +backed by the `package:test` [library](https://pub.dartlang.org/packages/test). + +Usage +----- + + +Update the dev dependencies in your `pubspec.yaml` to include the angular testing +and test packages: + +```yaml +dev_dependencies: + test: '^0.12.6' + angular2_testing: any + +``` + +Then in your test files, use angular2_testing helpers in place of `setUp` and `test`: + +```dart +import 'package:test/test.dart'; +import 'package:angular2_testing/angular2_testing.dart'; + +void main() { + // This must be called at the beginning of your tests. + initAngularTests(); + + // Initialize the injection tokens you will use in your tests. + setUpProviders(() => [provide(MyToken, useValue: 'my string'), TestService]); + + // You can then get tokens from the injector via ngSetUp and ngTest. + ngSetUp((TestService testService) { + testService.initialize(); + }); + + ngTest('can grab injected values', (@Inject(MyToken) token, TestService testService) { + expect(token, equals('my string')); + expect(testService.status, equals('ready')); + }); +} +``` + +Examples +-------- + +A sample test is available in `test/angular2_testing_test.dart`. diff --git a/modules_dart/angular2_testing/lib/angular2_testing.dart b/modules_dart/angular2_testing/lib/angular2_testing.dart new file mode 100644 index 0000000000..f13c098854 --- /dev/null +++ b/modules_dart/angular2_testing/lib/angular2_testing.dart @@ -0,0 +1,130 @@ +library angular2_testing.angular2_testing; + +import 'package:test/test.dart'; +import 'package:test/src/backend/invoker.dart'; +import 'package:test/src/backend/live_test.dart'; + +import 'package:angular2/angular2.dart'; +import 'package:angular2/src/core/di/injector.dart' show Injector; +import 'package:angular2/src/core/di/metadata.dart' show InjectMetadata; +import 'package:angular2/src/core/di/exceptions.dart' show NoAnnotationError; +import 'package:angular2/platform/browser_static.dart' show BrowserDomAdapter; +import 'package:angular2/src/core/reflection/reflection.dart'; +import 'package:angular2/src/core/reflection/reflection_capabilities.dart'; +import 'package:angular2/src/testing/test_injector.dart'; +export 'package:angular2/src/testing/test_component_builder.dart'; +export 'package:angular2/src/testing/test_injector.dart' show inject; + +/// One time initialization that must be done for Angular2 component +/// tests. Call before any test methods. +/// +/// Example: +/// +/// ``` +/// main() { +/// initAngularTests(); +/// group(...); +/// } +/// ``` +void initAngularTests() { + BrowserDomAdapter.makeCurrent(); + reflector.reflectionCapabilities = new ReflectionCapabilities(); +} + +/// Allows overriding default bindings defined in test_injector.dart. +/// +/// The given function must return a list of DI providers. +/// +/// Example: +/// +/// ``` +/// setUpProviders(() => [ +/// provide(Compiler, useClass: MockCompiler), +/// provide(SomeToken, useValue: myValue), +/// ]); +/// ``` +void setUpProviders(Iterable providerFactory()) { + setUp(() { + if (_currentInjector != null) { + throw 'setUpProviders was called after the injector had ' + 'been used in a setUp or test block. This invalidates the ' + 'test injector'; + } + _currentTestProviders.addAll(providerFactory()); + }); +} + + +dynamic _runInjectableFunction(Function fn) { + var params = reflector.parameters(fn); + List tokens = []; + for (var param in params) { + var token = null; + for (var paramMetadata in param) { + if (paramMetadata is Type) { + token = paramMetadata; + } else if (paramMetadata is InjectMetadata) { + token = paramMetadata.token; + } + } + if (token == null) { + throw new NoAnnotationError(fn, params); + } + tokens.add(token); + } + + if (_currentInjector == null) { + _currentInjector = createTestInjector(_currentTestProviders); + } + var injectFn = new FunctionWithParamTokens(tokens, fn, false); + return injectFn.execute(_currentInjector); +} + +/// Use the test injector to get bindings and run a function. +/// +/// Example: +/// +/// ``` +/// ngSetUp((SomeToken token) { +/// token.init(); +/// }); +/// ``` +void ngSetUp(Function fn) { + setUp(() async { + await _runInjectableFunction(fn); + }); +} + +/// Add a test which can use the test injector. +/// +/// Example: +/// +/// ``` +/// ngTest('description', (SomeToken token) { +/// expect(token, equals('expected')); +/// }); +/// ``` +void ngTest(String description, Function fn, + {String testOn, Timeout timeout, skip, Map onPlatform}) { + test(description, () async { + await _runInjectableFunction(fn); + }, testOn: testOn, timeout: timeout, skip: skip, onPlatform: onPlatform); +} + +final _providersExpando = new Expando>('Providers for the current test'); +final _injectorExpando = new Expando('Angular Injector for the current test'); + +List get _currentTestProviders { + if (_providersExpando[_currentTest] == null) { + return _providersExpando[_currentTest] = []; + } + return _providersExpando[_currentTest]; +} +Injector get _currentInjector => _injectorExpando[_currentTest]; +void set _currentInjector(Injector newInjector) { + _injectorExpando[_currentTest] = newInjector; +} + +// TODO: warning, the Invoker.current.liveTest is not a settled API and is +// subject to change in future versions of package:test. +LiveTest get _currentTest => Invoker.current.liveTest; diff --git a/modules_dart/angular2_testing/pubspec.yaml b/modules_dart/angular2_testing/pubspec.yaml new file mode 100644 index 0000000000..f3e520afbf --- /dev/null +++ b/modules_dart/angular2_testing/pubspec.yaml @@ -0,0 +1,8 @@ +name: angular2_testing +environment: + sdk: '>=1.10.0 <2.0.0' +dependencies: + angular2: + path: ../../dist/dart/angular2 +dev_dependencies: + test: '^0.12.6' diff --git a/modules_dart/angular2_testing/test/angular2_testing_test.dart b/modules_dart/angular2_testing/test/angular2_testing_test.dart new file mode 100644 index 0000000000..8a5640f873 --- /dev/null +++ b/modules_dart/angular2_testing/test/angular2_testing_test.dart @@ -0,0 +1,99 @@ +// Because Angular is using dart:html, we need these tests to run on an actual +// browser. This means that it should be run with `-p dartium` or `-p chrome`. +@TestOn('browser') +import 'package:angular2/angular2.dart' + show Component, View, NgFor, provide, Inject, Injectable, Optional; + +import 'package:test/test.dart'; +import 'package:angular2_testing/angular2_testing.dart'; + +// This is the component we will be testing. +@Component(selector: 'test-cmp') +@View(directives: const [NgFor]) +class TestComponent { + List items; + TestComponent() { + this.items = [1, 2]; + } +} + +@Injectable() +class TestService { + String status = 'not ready'; + + init() { + this.status = 'ready'; + } +} + +class MyToken {} + +const TEMPLATE = + '
{{item.toString()}};
'; + +void main() { + initAngularTests(); + + setUpProviders(() => [provide(MyToken, useValue: 'my string'), TestService]); + + test('normal function', () { + var string = 'foo,bar,baz'; + expect(string.split(','), equals(['foo', 'bar', 'baz'])); + }); + + ngTest('can grab injected values', (@Inject(MyToken) token, TestService testService) { + expect(token, equals('my string')); + expect(testService.status, equals('not ready')); + }); + + group('nested ngSetUp', () { + ngSetUp((TestService testService) { + testService.init(); + }); + + ngTest('ngSetUp modifies injected services', (TestService testService) { + expect(testService.status, equals('ready')); + }); + }); + + ngTest('create a component using the TestComponentBuilder', (TestComponentBuilder tcb) async { + var rootTC = await tcb + .overrideTemplate(TestComponent, TEMPLATE) + .createAsync(TestComponent); + + rootTC.detectChanges(); + expect(rootTC.debugElement.nativeElement.text, equals('1;2;')); + }); + + ngTest('should reflect added elements', (TestComponentBuilder tcb) async { + var rootTC = await tcb + .overrideTemplate(TestComponent, TEMPLATE) + .createAsync(TestComponent); + + rootTC.detectChanges(); + (rootTC.debugElement.componentInstance.items as List).add(3); + rootTC.detectChanges(); + + expect(rootTC.debugElement.nativeElement.text, equals('1;2;3;')); + }); + + group('expected failures', () { + ngTest('no type in param list', (notTyped) { + expect(1, equals(2)); + }); + + ngSetUp((TestService testService) { + testService.init(); + }); + + // This would fail, since setUpProviders is used after a call to ngSetUp has already + // initialized the injector. + group('nested', () { + setUpProviders(() => [TestService]); + + test('foo', () { + expect(1 + 1, equals(2)); + }); + }); + }, skip: 'expected failures'); +}