/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {ChangeDetectionStrategy, Compiler, Component, destroyPlatform, Directive, ElementRef, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, Output, SimpleChanges} from '@angular/core'; import {fakeAsync, tick, waitForAsync} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {downgradeComponent, UpgradeComponent, UpgradeModule} from '@angular/upgrade/static'; import * as angular from '../../../src/common/src/angular1'; import {$ROOT_SCOPE} from '../../../src/common/src/constants'; import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers'; import {$apply, bootstrap} from './static_test_helpers'; withEachNg1Version(() => { describe('downgrade ng2 component', () => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); it('should bind properties, events', waitForAsync(() => { const ng1Module = angular.module_('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['name'] = 'world'; $rootScope['dataA'] = 'A'; $rootScope['dataB'] = 'B'; $rootScope['modelA'] = 'initModelA'; $rootScope['modelB'] = 'initModelB'; $rootScope['eventA'] = '?'; $rootScope['eventB'] = '?'; }); @Component({ selector: 'ng2', inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], outputs: [ 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' ], template: 'ignore: {{ignore}}; ' + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' }) class Ng2Component implements OnChanges { ngOnChangesCount = 0; ignore = '-'; literal = '?'; interpolate = '?'; oneWayA = '?'; oneWayB = '?'; twoWayA = '?'; twoWayB = '?'; eventA = new EventEmitter(); eventB = new EventEmitter(); twoWayAEmitter = new EventEmitter(); twoWayBEmitter = new EventEmitter(); ngOnChanges(changes: SimpleChanges) { const assert = (prop: string, value: any) => { const propVal = (this as any)[prop]; if (propVal != value) { throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); } }; const assertChange = (prop: string, value: any) => { assert(prop, value); if (!changes[prop]) { throw new Error(`Changes record for '${prop}' not found.`); } const actualValue = changes[prop].currentValue; if (actualValue != value) { throw new Error(`Expected changes record for'${prop}' to be '${value}' but was '${ actualValue}'`); } }; switch (this.ngOnChangesCount++) { case 0: assert('ignore', '-'); assertChange('literal', 'Text'); assertChange('interpolate', 'Hello world'); assertChange('oneWayA', 'A'); assertChange('oneWayB', 'B'); assertChange('twoWayA', 'initModelA'); assertChange('twoWayB', 'initModelB'); this.twoWayAEmitter.emit('newA'); this.twoWayBEmitter.emit('newB'); this.eventA.emit('aFired'); this.eventB.emit('bFired'); break; case 1: assertChange('twoWayA', 'newA'); assertChange('twoWayB', 'newB'); break; case 2: assertChange('interpolate', 'Hello everyone'); break; default: throw new Error('Called too many times! ' + JSON.stringify(changes)); } } } ng1Module.directive('ng2', downgradeComponent({ component: Ng2Component, })); @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const element = html(`
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)) .toEqual( 'ignore: -; ' + 'literal: Text; interpolate: Hello world; ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); $apply(upgrade, 'name = "everyone"'); expect(multiTrim(document.body.textContent)) .toEqual( 'ignore: -; ' + 'literal: Text; interpolate: Hello everyone; ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); }); })); it('should bind properties to onpush components', waitForAsync(() => { const ng1Module = angular.module_('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['dataB'] = 'B'; }); @Component({ selector: 'ng2', inputs: ['oneWayB'], template: 'oneWayB: {{oneWayB}}', changeDetection: ChangeDetectionStrategy.OnPush }) class Ng2Component { ngOnChangesCount = 0; oneWayB = '?'; } ng1Module.directive('ng2', downgradeComponent({ component: Ng2Component, })); @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const element = html(`
`); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)).toEqual('oneWayB: B'); $apply(upgrade, 'dataB= "everyone"'); expect(multiTrim(document.body.textContent)).toEqual('oneWayB: everyone'); }); })); it('should support two-way binding and event listener', waitForAsync(() => { const listenerSpy = jasmine.createSpy('$rootScope.listener'); const ng1Module = angular.module_('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['value'] = 'world'; $rootScope['listener'] = listenerSpy; }); @Component({selector: 'ng2', template: `model: {{model}};`}) class Ng2Component implements OnChanges { ngOnChangesCount = 0; @Input() model = '?'; @Output() modelChange = new EventEmitter(); ngOnChanges(changes: SimpleChanges) { switch (this.ngOnChangesCount++) { case 0: expect(changes.model.currentValue).toBe('world'); this.modelChange.emit('newC'); break; case 1: expect(changes.model.currentValue).toBe('newC'); break; default: throw new Error('Called too many times! ' + JSON.stringify(changes)); } } } ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const element = html(`
| value: {{value}}
`); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); expect(listenerSpy).toHaveBeenCalledWith('newC'); }); })); it('should run change-detection on every digest (by default)', waitForAsync(() => { let ng2Component: Ng2Component; @Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'}) class Ng2Component { @Input() value1 = -1; @Input() value2 = -1; constructor() { ng2Component = this; } } @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive('ng2', downgradeComponent({component: Ng2Component})) .run(($rootScope: angular.IRootScopeService) => { $rootScope.value1 = 0; $rootScope.value2 = 0; }); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService; expect(element.textContent).toBe('0 | 0'); // Digest should invoke CD $rootScope.$digest(); $rootScope.$digest(); expect(element.textContent).toBe('0 | 0'); // Internal changes should be detected on digest ng2Component.value1 = 1; ng2Component.value2 = 2; $rootScope.$digest(); expect(element.textContent).toBe('1 | 2'); // Digest should propagate change in prop-bound input $rootScope.$apply('value1 = 3'); expect(element.textContent).toBe('3 | 2'); // Digest should propagate change in attr-bound input ng2Component.value1 = 4; $rootScope.$apply('value2 = 5'); expect(element.textContent).toBe('4 | 5'); // Digest should propagate changes that happened before the digest $rootScope.value1 = 6; expect(element.textContent).toBe('4 | 5'); $rootScope.$digest(); expect(element.textContent).toBe('6 | 5'); }); })); it('should not run change-detection on every digest when opted out', waitForAsync(() => { let ng2Component: Ng2Component; @Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'}) class Ng2Component { @Input() value1 = -1; @Input() value2 = -1; constructor() { ng2Component = this; } } @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive( 'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false})) .run(($rootScope: angular.IRootScopeService) => { $rootScope.value1 = 0; $rootScope.value2 = 0; }); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService; expect(element.textContent).toBe('0 | 0'); // Digest should not invoke CD $rootScope.$digest(); $rootScope.$digest(); expect(element.textContent).toBe('0 | 0'); // Digest should not invoke CD, even if component values have changed (internally) ng2Component.value1 = 1; ng2Component.value2 = 2; $rootScope.$digest(); expect(element.textContent).toBe('0 | 0'); // Digest should invoke CD, if prop-bound input has changed $rootScope.$apply('value1 = 3'); expect(element.textContent).toBe('3 | 2'); // Digest should invoke CD, if attr-bound input has changed ng2Component.value1 = 4; $rootScope.$apply('value2 = 5'); expect(element.textContent).toBe('4 | 5'); // Digest should invoke CD, if input has changed before the digest $rootScope.value1 = 6; $rootScope.$digest(); expect(element.textContent).toBe('6 | 5'); }); })); it('should still run normal Angular change-detection regardless of `propagateDigest`', fakeAsync(() => { let ng2Component: Ng2Component; @Component({selector: 'ng2', template: '{{ value }}'}) class Ng2Component { value = 'foo'; constructor() { setTimeout(() => this.value = 'bar', 1000); } } @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive( 'ng2A', downgradeComponent({component: Ng2Component, propagateDigest: true})) .directive( 'ng2B', downgradeComponent({component: Ng2Component, propagateDigest: false})); const element = html(' | '); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { expect(element.textContent).toBe('foo | foo'); tick(1000); expect(element.textContent).toBe('bar | bar'); }); })); it('should initialize inputs in time for `ngOnChanges`', waitForAsync(() => { @Component({ selector: 'ng2', template: ` ngOnChangesCount: {{ ngOnChangesCount }} | firstChangesCount: {{ firstChangesCount }} | initialValue: {{ initialValue }}` }) class Ng2Component implements OnChanges { ngOnChangesCount = 0; firstChangesCount = 0; // TODO(issue/24571): remove '!'. initialValue!: string; // TODO(issue/24571): remove '!'. @Input() foo!: string; ngOnChanges(changes: SimpleChanges) { this.ngOnChangesCount++; if (this.ngOnChangesCount === 1) { this.initialValue = this.foo; } if (changes['foo'] && changes['foo'].isFirstChange()) { this.firstChangesCount++; } } } @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng2Component], entryComponents: [Ng2Component] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []).directive( 'ng2', downgradeComponent({component: Ng2Component})); const element = html(` `); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { const nodes = element.querySelectorAll('ng2'); const expectedTextWith = (value: string) => `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); }); })); it('should bind to ng-model', waitForAsync(() => { const ng1Module = angular.module_('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['modelA'] = 'A'; }); let ng2Instance: Ng2; @Component({selector: 'ng2', template: '{{_value}}'}) class Ng2 { private _value: any = ''; private _onChangeCallback: (_: any) => void = () => {}; private _onTouchedCallback: () => void = () => {}; constructor() { ng2Instance = this; } writeValue(value: any) { this._value = value; } registerOnChange(fn: any) { this._onChangeCallback = fn; } registerOnTouched(fn: any) { this._onTouchedCallback = fn; } doTouch() { this._onTouchedCallback(); } doChange(newValue: string) { this._value = newValue; this._onChangeCallback(newValue); } } ng1Module.directive('ng2', downgradeComponent({component: Ng2})); const element = html(`
| {{modelA}}
`); @NgModule( {declarations: [Ng2], entryComponents: [Ng2], imports: [BrowserModule, UpgradeModule]}) class Ng2Module { ngDoBootstrap() {} } platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => { const adapter = ref.injector.get(UpgradeModule) as UpgradeModule; adapter.bootstrap(element, [ng1Module.name]); const $rootScope = adapter.$injector.get('$rootScope'); expect(multiTrim(document.body.textContent)).toEqual('A | A'); $rootScope.modelA = 'B'; $rootScope.$apply(); expect(multiTrim(document.body.textContent)).toEqual('B | B'); ng2Instance.doChange('C'); expect($rootScope.modelA).toBe('C'); expect(multiTrim(document.body.textContent)).toEqual('C | C'); const downgradedElement = document.body.querySelector('ng2'); expect(downgradedElement.classList.contains('ng-touched')).toBe(false); ng2Instance.doTouch(); $rootScope.$apply(); expect(downgradedElement.classList.contains('ng-touched')).toBe(true); }); })); it('should properly run cleanup when ng1 directive is destroyed', waitForAsync(() => { let destroyed = false; @Component({selector: 'ng2', template: ''}) class Ng2Component implements OnDestroy { ngOnDestroy() { destroyed = true; } } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive( 'ng1', () => { return {template: '
'}; }) .directive('ng2', downgradeComponent({component: Ng2Component})); const element = html(''); platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => { const adapter = ref.injector.get(UpgradeModule) as UpgradeModule; adapter.bootstrap(element, [ng1Module.name]); const ng2Element = angular.element(element.querySelector('ng2') as Element); const ng2Descendants = Array.from(element.querySelectorAll('ng2 li')).map(angular.element); let ng2ElementDestroyed = false; let ng2DescendantsDestroyed = [false, false]; ng2Element.data!('test', 42); ng2Descendants.forEach((elem, i) => elem.data!('test', i)); ng2Element.on!('$destroy', () => ng2ElementDestroyed = true); ng2Descendants.forEach( (elem, i) => elem.on!('$destroy', () => ng2DescendantsDestroyed[i] = true)); expect(element.textContent).toBe('test1test2'); expect(destroyed).toBe(false); expect(ng2Element.data!('test')).toBe(42); ng2Descendants.forEach((elem, i) => expect(elem.data!('test')).toBe(i)); expect(ng2ElementDestroyed).toBe(false); expect(ng2DescendantsDestroyed).toEqual([false, false]); const $rootScope = adapter.$injector.get('$rootScope'); $rootScope.$apply('destroyIt = true'); expect(element.textContent).toBe(''); expect(destroyed).toBe(true); expect(ng2Element.data!('test')).toBeUndefined(); ng2Descendants.forEach(elem => expect(elem.data!('test')).toBeUndefined()); expect(ng2ElementDestroyed).toBe(true); expect(ng2DescendantsDestroyed).toEqual([true, true]); }); })); it('should properly run cleanup with multiple levels of nesting', waitForAsync(() => { let destroyed = false; @Component({ selector: 'ng2-outer', template: '
', }) class Ng2OuterComponent { @Input() destroyIt = false; } @Component({selector: 'ng2-inner', template: 'test'}) class Ng2InnerComponent implements OnDestroy { ngOnDestroy() { destroyed = true; } } @Directive({selector: 'ng1'}) class Ng1ComponentFacade extends UpgradeComponent { constructor(elementRef: ElementRef, injector: Injector) { super('ng1', elementRef, injector); } } @NgModule({ imports: [BrowserModule, UpgradeModule], declarations: [Ng1ComponentFacade, Ng2InnerComponent, Ng2OuterComponent], entryComponents: [Ng2InnerComponent, Ng2OuterComponent], }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive('ng1', () => ({template: ''})) .directive('ng2Inner', downgradeComponent({component: Ng2InnerComponent})) .directive('ng2Outer', downgradeComponent({component: Ng2OuterComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { expect(element.textContent).toBe('test'); expect(destroyed).toBe(false); $apply(upgrade, 'destroyIt = true'); expect(element.textContent).toBe(''); expect(destroyed).toBe(true); }); })); it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => { @Component({selector: 'ng2', template: 'NG2'}) class Ng2Component { } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule], }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .component('ng1', {template: ''}) .directive('ng2', downgradeComponent({component: Ng2Component})); const element = html('
'); const platformRef = platformBrowserDynamic(); platformRef.bootstrapModule(Ng2Module).then(ref => { const upgrade = ref.injector.get(UpgradeModule); upgrade.bootstrap(element, [ng1Module.name]); const $rootScope: angular.IRootScopeService = upgrade.$injector.get($ROOT_SCOPE); const rootScopeDestroySpy = spyOn($rootScope, '$destroy'); const appElem = angular.element(element); const ng1Elem = angular.element(element.querySelector('ng1') as Element); const ng2Elem = angular.element(element.querySelector('ng2') as Element); const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element); // Attach data to all elements. appElem.data!('testData', 1); ng1Elem.data!('testData', 2); ng2Elem.data!('testData', 3); ng2ChildElem.data!('testData', 4); // Verify data can be retrieved. expect(appElem.data!('testData')).toBe(1); expect(ng1Elem.data!('testData')).toBe(2); expect(ng2Elem.data!('testData')).toBe(3); expect(ng2ChildElem.data!('testData')).toBe(4); expect(rootScopeDestroySpy).not.toHaveBeenCalled(); // Destroy `PlatformRef`. platformRef.destroy(); // Verify `$rootScope` has been destroyed and data has been cleaned up. expect(rootScopeDestroySpy).toHaveBeenCalled(); expect(appElem.data!('testData')).toBeUndefined(); expect(ng1Elem.data!('testData')).toBeUndefined(); expect(ng2Elem.data!('testData')).toBeUndefined(); expect(ng2ChildElem.data!('testData')).toBeUndefined(); }); })); it('should work when compiled outside the dom (by fallback to the root ng2.injector)', waitForAsync(() => { @Component({selector: 'ng2', template: 'test'}) class Ng2Component { } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive( 'ng1', [ '$compile', ($compile: angular.ICompileService) => { return { link: function( $scope: angular.IScope, $element: angular.IAugmentedJQuery, $attrs: angular.IAttributes) { // here we compile some HTML that contains a downgraded component // since it is not currently in the DOM it is not able to "require" // an ng2 injector so it should use the `moduleInjector` instead. const compiled = $compile(''); const template = compiled($scope); $element.append!(template); } }; } ]) .directive('ng2', downgradeComponent({component: Ng2Component})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { // the fact that the body contains the correct text means that the // downgraded component was able to access the moduleInjector // (since there is no other injector in this system) expect(multiTrim(document.body.textContent)).toEqual('test'); }); })); it('should allow attribute selectors for downgraded components', waitForAsync(() => { @Component({selector: '[itWorks]', template: 'It works'}) class WorksComponent { } @NgModule({ declarations: [WorksComponent], entryComponents: [WorksComponent], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []).directive( 'worksComponent', downgradeComponent({component: WorksComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)).toBe('It works'); }); })); it('should allow attribute selectors for components in ng2', waitForAsync(() => { @Component({selector: '[itWorks]', template: 'It works'}) class WorksComponent { } @Component({selector: 'root-component', template: '!'}) class RootComponent { } @NgModule({ declarations: [RootComponent, WorksComponent], entryComponents: [RootComponent], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []).directive( 'rootComponent', downgradeComponent({component: RootComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(multiTrim(document.body.textContent)).toBe('It works!'); }); })); it('should respect hierarchical dependency injection for ng2', waitForAsync(() => { @Component({selector: 'parent', template: 'parent()'}) class ParentComponent { } @Component({selector: 'child', template: 'child'}) class ChildComponent { constructor(parent: ParentComponent) {} } @NgModule({ declarations: [ParentComponent, ChildComponent], entryComponents: [ParentComponent, ChildComponent], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive('parent', downgradeComponent({component: ParentComponent})) .directive('child', downgradeComponent({component: ChildComponent})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { expect(multiTrim(document.body.textContent)).toBe('parent(child)'); }); })); it('should be compiled synchronously, if possible', waitForAsync(() => { @Component({selector: 'ng2A', template: ''}) class Ng2ComponentA { } @Component({selector: 'ng2B', template: '{{ \'Ng2 template\' }}'}) class Ng2ComponentB { } @NgModule({ declarations: [Ng2ComponentA, Ng2ComponentB], entryComponents: [Ng2ComponentA, Ng2ComponentB], imports: [BrowserModule, UpgradeModule], }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})) .directive('ng2B', downgradeComponent({component: Ng2ComponentB})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { expect(element.textContent).toBe('Ng2 template'); }); })); it('should work with ng2 lazy loaded components', waitForAsync(() => { let componentInjector: Injector; @Component({selector: 'ng2', template: ''}) class Ng2Component { constructor(injector: Injector) { componentInjector = injector; } } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule], }) class Ng2Module { ngDoBootstrap() {} } @Component({template: ''}) class LazyLoadedComponent { constructor(public module: NgModuleRef) {} } @NgModule({ declarations: [LazyLoadedComponent], entryComponents: [LazyLoadedComponent], }) class LazyLoadedModule { } const ng1Module = angular.module_('ng1', []).directive( 'ng2', downgradeComponent({component: Ng2Component})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { const modInjector = upgrade.injector; // Emulate the router lazy loading a module and creating a component const compiler = modInjector.get(Compiler); const modFactory = compiler.compileModuleSync(LazyLoadedModule); const childMod = modFactory.create(modInjector); const cmpFactory = childMod.componentFactoryResolver.resolveComponentFactory(LazyLoadedComponent)!; const lazyCmp = cmpFactory.create(componentInjector); expect(lazyCmp.instance.module.injector === childMod.injector).toBe(true); }); })); it('should throw if `downgradedModule` is specified', waitForAsync(() => { @Component({selector: 'ng2', template: ''}) class Ng2Component { } @NgModule({ declarations: [Ng2Component], entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule], }) class Ng2Module { ngDoBootstrap() {} } const ng1Module = angular.module_('ng1', []).directive( 'ng2', downgradeComponent({component: Ng2Component, downgradedModule: 'foo'})); const element = html(''); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module) .then( () => { throw new Error('Expected bootstraping to fail.'); }, err => expect(err.message) .toBe( 'Error while instantiating component \'Ng2Component\': \'downgradedModule\' ' + 'unexpectedly specified.\n' + 'You should not specify a value for \'downgradedModule\', unless you are ' + 'downgrading more than one Angular module (via \'downgradeModule()\').')); })); }); });