From b9c6df6da70055f77409be41c6f0f2a7747ce50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Wed, 2 Jan 2019 15:12:36 -0800 Subject: [PATCH] fix(ivy): use NgZone.onStable when bootstraped using PlatformRef (#27898) PR Close #27898 --- integration/_payload-limits.json | 2 +- packages/core/src/application_module.ts | 23 +- packages/router/test/integration.spec.ts | 293 +++++++++--------- packages/upgrade/test/dynamic/upgrade_spec.ts | 172 +++++----- .../integration/change_detection_spec.ts | 183 ++++++----- 5 files changed, 349 insertions(+), 324 deletions(-) diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 6a5f9019ac..9691a8dda6 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime": 1497, - "main": 187112, + "main": 187134, "polyfills": 59608 } } diff --git a/packages/core/src/application_module.ts b/packages/core/src/application_module.ts index a809f5c8fb..a28a3905a6 100644 --- a/packages/core/src/application_module.ts +++ b/packages/core/src/application_module.ts @@ -11,13 +11,14 @@ import {ApplicationRef} from './application_ref'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; import {Console} from './console'; -import {InjectionToken, Injector, StaticProvider} from './di'; +import {Injector, StaticProvider} from './di'; import {Inject, Optional, SkipSelf} from './di/metadata'; import {ErrorHandler} from './error_handler'; import {LOCALE_ID} from './i18n/tokens'; import {ComponentFactoryResolver} from './linker'; import {Compiler} from './linker/compiler'; import {NgModule} from './metadata'; +import {SCHEDULER} from './render3/component_ref'; import {NgZone} from './zone'; export function _iterableDiffersFactory() { @@ -43,6 +44,7 @@ export const APPLICATION_MODULE_PROVIDERS: StaticProvider[] = [ deps: [NgZone, Console, Injector, ErrorHandler, ComponentFactoryResolver, ApplicationInitStatus] }, + {provide: SCHEDULER, deps: [NgZone], useFactory: zoneSchedulerFactory}, { provide: ApplicationInitStatus, useClass: ApplicationInitStatus, @@ -59,6 +61,25 @@ export const APPLICATION_MODULE_PROVIDERS: StaticProvider[] = [ }, ]; +/** + * Schedule work at next available slot. + * + * In Ivy this is just `requestAnimationFrame`. For compatibility reasons when bootstrapped + * using `platformRef.bootstrap` we need to use `NgZone.onStable` as the scheduling mechanism. + * This overrides the scheduling mechanism in Ivy to `NgZone.onStable`. + * + * @param ngZone NgZone to use for scheduling. + */ +export function zoneSchedulerFactory(ngZone: NgZone): (fn: () => void) => void { + let queue: (() => void)[] = []; + ngZone.onStable.subscribe(() => { + while (queue.length) { + queue.pop() !(); + } + }); + return function(fn: () => void) { queue.push(fn); }; +} + /** * Configures the root injector for an app with * providers of `@angular/core` dependencies that `ApplicationRef` needs diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 37fb3db386..27f492531f 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -451,21 +451,22 @@ describe('Integration', () => { expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); }))); - it('should work when an outlet is in an ngIf', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unknown/maybe FW-918') + .it('should work when an outlet is in an ngIf', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'child', - component: OutletInNgIf, - children: [{path: 'simple', component: SimpleCmp}] - }]); + router.resetConfig([{ + path: 'child', + component: OutletInNgIf, + children: [{path: 'simple', component: SimpleCmp}] + }]); - router.navigateByUrl('/child/simple'); - advance(fixture); + router.navigateByUrl('/child/simple'); + advance(fixture); - expect(location.path()).toEqual('/child/simple'); - }))); + expect(location.path()).toEqual('/child/simple'); + }))); it('should work when an outlet is added/removed', fakeAsync(() => { @Component({ @@ -634,46 +635,49 @@ describe('Integration', () => { expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); }))); - it('should navigate back and forward', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unknown/maybe FW-918') + .it('should navigate back and forward', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: - [{path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}] - }]); + router.resetConfig([{ + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp} + ] + }]); - let event: NavigationStart; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - event = e; - } - }); + let event: NavigationStart; + router.events.subscribe(e => { + if (e instanceof NavigationStart) { + event = e; + } + }); - router.navigateByUrl('/team/33/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - const simpleNavStart = event !; + router.navigateByUrl('/team/33/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + const simpleNavStart = event !; - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - const userVictorNavStart = event !; + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + const userVictorNavStart = event !; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - expect(event !.navigationTrigger).toEqual('hashchange'); - expect(event !.restoredState !.navigationId).toEqual(simpleNavStart.id); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + expect(event !.navigationTrigger).toEqual('hashchange'); + expect(event !.restoredState !.navigationId).toEqual(simpleNavStart.id); - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/team/22/user/victor'); - expect(event !.navigationTrigger).toEqual('hashchange'); - expect(event !.restoredState !.navigationId).toEqual(userVictorNavStart.id); - }))); + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/team/22/user/victor'); + expect(event !.navigationTrigger).toEqual('hashchange'); + expect(event !.restoredState !.navigationId).toEqual(userVictorNavStart.id); + }))); it('should navigate to the same url when config changes', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { @@ -1004,34 +1008,36 @@ describe('Integration', () => { ]); }))); - it('should handle failed navigations gracefully', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unknown/maybe FW-918') + .it('should handle failed navigations gracefully', + fakeAsync(inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'user/:name', component: UserCmp}]); + router.resetConfig([{path: 'user/:name', component: UserCmp}]); - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); - let e: any; - router.navigateByUrl('/invalid') !.catch(_ => e = _); - advance(fixture); - expect(e.message).toContain('Cannot match any routes'); + let e: any; + router.navigateByUrl('/invalid') !.catch(_ => e = _); + advance(fixture); + expect(e.message).toContain('Cannot match any routes'); - router.navigateByUrl('/user/fedor'); - advance(fixture); + router.navigateByUrl('/user/fedor'); + advance(fixture); - expect(fixture.nativeElement).toHaveText('user fedor'); + expect(fixture.nativeElement).toHaveText('user fedor'); - expectEvents(recordedEvents, [ - [NavigationStart, '/invalid'], [NavigationError, '/invalid'], + expectEvents(recordedEvents, [ + [NavigationStart, '/invalid'], [NavigationError, '/invalid'], - [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], - [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart], - [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], - [ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd], - [NavigationEnd, '/user/fedor'] - ]); - }))); + [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], + [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart], + [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], + [ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd], + [NavigationEnd, '/user/fedor'] + ]); + }))); // Errors should behave the same for both deferred and eager URL update strategies ['deferred', 'eager'].forEach((strat: any) => { @@ -1879,45 +1885,50 @@ describe('Integration', () => { }); describe('redirects', () => { - it('should work', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unkwnown/maybe FW-918') + .it('should work', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp} - ]); + router.resetConfig([ + {path: 'old/team/:id', redirectTo: 'team/:id'}, + {path: 'team/:id', component: TeamCmp} + ]); - router.navigateByUrl('old/team/22'); - advance(fixture); + router.navigateByUrl('old/team/22'); + advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + expect(location.path()).toEqual('/team/22'); + }))); - it('should update Navigation object after redirects are applied', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); - let initialUrl, afterRedirectUrl; + fixmeIvy('unkwnown/maybe FW-918') + .it('should update Navigation object after redirects are applied', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + let initialUrl, afterRedirectUrl; - router.resetConfig([ - {path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp} - ]); + router.resetConfig([ + {path: 'old/team/:id', redirectTo: 'team/:id'}, + {path: 'team/:id', component: TeamCmp} + ]); - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - const navigation = router.getCurrentNavigation(); - initialUrl = navigation && navigation.finalUrl; - } - if (e instanceof RoutesRecognized) { - const navigation = router.getCurrentNavigation(); - afterRedirectUrl = navigation && navigation.finalUrl; - } - }); + router.events.subscribe(e => { + if (e instanceof NavigationStart) { + const navigation = router.getCurrentNavigation(); + initialUrl = navigation && navigation.finalUrl; + } + if (e instanceof RoutesRecognized) { + const navigation = router.getCurrentNavigation(); + afterRedirectUrl = navigation && navigation.finalUrl; + } + }); - router.navigateByUrl('old/team/22'); - advance(fixture); + router.navigateByUrl('old/team/22'); + advance(fixture); - expect(initialUrl).toBeUndefined(); - expect(router.serializeUrl(afterRedirectUrl as any)).toBe('/team/22'); - }))); + expect(initialUrl).toBeUndefined(); + expect(router.serializeUrl(afterRedirectUrl as any)).toBe('/team/22'); + }))); it('should not break the back button when trigger by location change', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { @@ -2022,17 +2033,18 @@ describe('Integration', () => { }); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unknown/maybe FW-918') + .it('works', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]); + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]); - router.navigateByUrl('/team/22'); - advance(fixture); - - expect(location.path()).toEqual('/team/22'); - }))); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }))); }); describe('should work when given a class', () => { @@ -2044,17 +2056,19 @@ describe('Integration', () => { beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unknown/maybe FW-918') + .it('works', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]); + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]); - router.navigateByUrl('/team/22'); - advance(fixture); + router.navigateByUrl('/team/22'); + advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + expect(location.path()).toEqual('/team/22'); + }))); }); describe('should work when returns an observable', () => { @@ -3343,37 +3357,38 @@ describe('Integration', () => { }); describe('route events', () => { - it('should fire matching (Child)ActivationStart/End events', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + fixmeIvy('unknown/maybe FW-918') + .it('should fire matching (Child)ActivationStart/End events', + fakeAsync(inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'user/:name', component: UserCmp}]); + router.resetConfig([{path: 'user/:name', component: UserCmp}]); - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.forEach(e => recordedEvents.push(e)); - router.navigateByUrl('/user/fedor'); - advance(fixture); + router.navigateByUrl('/user/fedor'); + advance(fixture); - expect(fixture.nativeElement).toHaveText('user fedor'); - expect(recordedEvents[3] instanceof ChildActivationStart).toBe(true); - expect(recordedEvents[3].snapshot).toBe(recordedEvents[9].snapshot.root); - expect(recordedEvents[9] instanceof ChildActivationEnd).toBe(true); - expect(recordedEvents[9].snapshot).toBe(recordedEvents[9].snapshot.root); + expect(fixture.nativeElement).toHaveText('user fedor'); + expect(recordedEvents[3] instanceof ChildActivationStart).toBe(true); + expect(recordedEvents[3].snapshot).toBe(recordedEvents[9].snapshot.root); + expect(recordedEvents[9] instanceof ChildActivationEnd).toBe(true); + expect(recordedEvents[9].snapshot).toBe(recordedEvents[9].snapshot.root); - expect(recordedEvents[4] instanceof ActivationStart).toBe(true); - expect(recordedEvents[4].snapshot.routeConfig.path).toBe('user/:name'); - expect(recordedEvents[8] instanceof ActivationEnd).toBe(true); - expect(recordedEvents[8].snapshot.routeConfig.path).toBe('user/:name'); + expect(recordedEvents[4] instanceof ActivationStart).toBe(true); + expect(recordedEvents[4].snapshot.routeConfig.path).toBe('user/:name'); + expect(recordedEvents[8] instanceof ActivationEnd).toBe(true); + expect(recordedEvents[8].snapshot.routeConfig.path).toBe('user/:name'); - expectEvents(recordedEvents, [ - [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], - [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart], - [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], - [ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd], - [NavigationEnd, '/user/fedor'] - ]); - }))); + expectEvents(recordedEvents, [ + [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], + [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart], + [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], + [ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd], + [NavigationEnd, '/user/fedor'] + ]); + }))); it('should allow redirection in NavigationStart', fakeAsync(inject([Router], (router: Router) => { diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index b3bb64dee7..29a1ec079d 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -170,110 +170,104 @@ withEachNg1Version(() => { }); describe('scope/component change-detection', () => { - it('should interleave scope and component expressions', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const ng1Module = angular.module('ng1', []); - const log: string[] = []; - const l = (value: string) => { - log.push(value); - return value + ';'; - }; - - ng1Module.directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'})); - ng1Module.directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'})); - ng1Module.run(($rootScope: any) => { - $rootScope.l = l; - $rootScope.reset = () => log.length = 0; - }); - - @Component({ - selector: 'ng2', - template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` - }) - class Ng2 { - l: any; - constructor() { this.l = l; } - } - - @NgModule({ - declarations: - [adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2], - imports: [BrowserModule], - }) - class Ng2Module { - } - - ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); - - const element = - html('
{{reset(); l(\'1A\');}}{{l(\'1B\')}}{{l(\'1C\')}}
'); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;'); - // https://github.com/angular/angular.js/issues/12983 - expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); - ref.dispose(); - }); - })); - - fixmeIvy( - 'FW-712: Rendering is being run on next "animation frame" rather than "Zone.microTaskEmpty" trigger') - .it('should propagate changes to a downgraded component inside the ngZone', async(() => { - let appComponent: AppComponent; - let upgradeRef: UpgradeAdapterRef; + 'FW-918: Create API and mental model to work with Host Element; and ChangeDetections') + .it('should interleave scope and component expressions', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const ng1Module = angular.module('ng1', []); + const log: string[] = []; + const l = (value: string) => { + log.push(value); + return value + ';'; + }; - @Component({selector: 'my-app', template: ''}) - class AppComponent { - value?: number; - constructor() { appComponent = this; } - } + ng1Module.directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'})); + ng1Module.directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'})); + ng1Module.run(($rootScope: any) => { + $rootScope.l = l; + $rootScope.reset = () => log.length = 0; + }); @Component({ - selector: 'my-child', - template: '
{{valueFromPromise}}', + selector: 'ng2', + template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` }) - class ChildComponent { - valueFromPromise?: number; - @Input() - set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); } - - constructor(private zone: NgZone) {} - - ngOnChanges(changes: SimpleChanges) { - if (changes['value'].isFirstChange()) return; - - // HACK(ivy): Using setTimeout allows this test to pass but hides the ivy - // renderer timing BC. - // setTimeout(() => { - // expect(element.textContent).toEqual('5'); - // upgradeRef.dispose(); - // }, 0); - this.zone.onMicrotaskEmpty.subscribe(() => { - expect(element.textContent).toEqual('5'); - upgradeRef.dispose(); - }); - - Promise.resolve().then( - () => this.valueFromPromise = changes['value'].currentValue); - } + class Ng2 { + l: any; + constructor() { this.l = l; } } - @NgModule({declarations: [AppComponent, ChildComponent], imports: [BrowserModule]}) + @NgModule({ + declarations: [ + adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2 + ], + imports: [BrowserModule], + }) class Ng2Module { } - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const ng1Module = angular.module('ng1', []).directive( - 'myApp', adapter.downgradeNg2Component(AppComponent)); - - const element = html(''); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + const element = + html('
{{reset(); l(\'1A\');}}{{l(\'1B\')}}{{l(\'1C\')}}
'); adapter.bootstrap(element, ['ng1']).ready((ref) => { - upgradeRef = ref; - appComponent.value = 5; + expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;'); + // https://github.com/angular/angular.js/issues/12983 + expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); + ref.dispose(); }); })); + + it('should propagate changes to a downgraded component inside the ngZone', async(() => { + let appComponent: AppComponent; + let upgradeRef: UpgradeAdapterRef; + + @Component({selector: 'my-app', template: ''}) + class AppComponent { + value?: number; + constructor() { appComponent = this; } + } + + @Component({ + selector: 'my-child', + template: '
{{valueFromPromise}}', + }) + class ChildComponent { + valueFromPromise?: number; + @Input() + set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); } + + constructor(private zone: NgZone) {} + + ngOnChanges(changes: SimpleChanges) { + if (changes['value'].isFirstChange()) return; + + this.zone.onMicrotaskEmpty.subscribe(() => { + expect(element.textContent).toEqual('5'); + upgradeRef.dispose(); + }); + + Promise.resolve().then(() => this.valueFromPromise = changes['value'].currentValue); + } + } + + @NgModule({declarations: [AppComponent, ChildComponent], imports: [BrowserModule]}) + class Ng2Module { + } + + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const ng1Module = angular.module('ng1', []).directive( + 'myApp', adapter.downgradeNg2Component(AppComponent)); + + const element = html(''); + + adapter.bootstrap(element, ['ng1']).ready((ref) => { + upgradeRef = ref; + appComponent.value = 5; + }); + })); + // This test demonstrates https://github.com/angular/angular/issues/6385 // which was invalidly fixed by https://github.com/angular/angular/pull/6386 // it('should not trigger $digest from an async operation in a watcher', async(() => { diff --git a/packages/upgrade/test/static/integration/change_detection_spec.ts b/packages/upgrade/test/static/integration/change_detection_spec.ts index 9268165ce4..b74c08c4ef 100644 --- a/packages/upgrade/test/static/integration/change_detection_spec.ts +++ b/packages/upgrade/test/static/integration/change_detection_spec.ts @@ -21,116 +21,111 @@ withEachNg1Version(() => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); - it('should interleave scope and component expressions', async(() => { - const log: string[] = []; - const l = (value: string) => { - log.push(value); - return value + ';'; - }; + fixmeIvy('FW-918: Create API and mental model to work with Host Element; and ChangeDetections') + .it('should interleave scope and component expressions', async(() => { + const log: string[] = []; + const l = (value: string) => { + log.push(value); + return value + ';'; + }; - @Directive({selector: 'ng1a'}) - class Ng1aComponent extends UpgradeComponent { - constructor(elementRef: ElementRef, injector: Injector) { - super('ng1a', elementRef, injector); - } - } - - @Directive({selector: 'ng1b'}) - class Ng1bComponent extends UpgradeComponent { - constructor(elementRef: ElementRef, injector: Injector) { - super('ng1b', elementRef, injector); - } - } - - @Component({ - selector: 'ng2', - template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` - }) - class Ng2Component { - l = l; - } - - @NgModule({ - declarations: [Ng1aComponent, Ng1bComponent, Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule, UpgradeModule] - }) - class Ng2Module { - ngDoBootstrap() {} - } - - const ng1Module = angular.module('ng1', []) - .directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'})) - .directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'})) - .directive('ng2', downgradeComponent({component: Ng2Component})) - .run(($rootScope: angular.IRootScopeService) => { - $rootScope.l = l; - $rootScope.reset = () => log.length = 0; - }); - - const element = - html('
{{reset(); l(\'1A\');}}{{l(\'1B\')}}{{l(\'1C\')}}
'); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { - expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;'); - expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); - }); - })); - - fixmeIvy( - 'FW-712: Rendering is being run on next "animation frame" rather than "Zone.microTaskEmpty" trigger') - .it('should propagate changes to a downgraded component inside the ngZone', async(() => { - const element = html(''); - let appComponent: AppComponent; - - @Component({selector: 'my-app', template: ''}) - class AppComponent { - value?: number; - constructor() { appComponent = this; } - } - - @Component({ - selector: 'my-child', - template: '
{{ valueFromPromise }}
', - }) - class ChildComponent { - valueFromPromise?: number; - @Input() - set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); } - - constructor(private zone: NgZone) {} - - ngOnChanges(changes: SimpleChanges) { - if (changes['value'].isFirstChange()) return; - - // HACK(ivy): Using setTimeout allows this test to pass but hides the ivy renderer - // timing BC. - // setTimeout(() => expect(element.textContent).toEqual('5'), 0); - this.zone.onMicrotaskEmpty.subscribe( - () => { expect(element.textContent).toEqual('5'); }); - - // Create a micro-task to update the value to be rendered asynchronously. - Promise.resolve().then( - () => this.valueFromPromise = changes['value'].currentValue); + @Directive({selector: 'ng1a'}) + class Ng1aComponent extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1a', elementRef, injector); } } + @Directive({selector: 'ng1b'}) + class Ng1bComponent extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1b', elementRef, injector); + } + } + + @Component({ + selector: 'ng2', + template: `{{l('2A')}}{{l('2B')}}{{l('2C')}}` + }) + class Ng2Component { + l = l; + } + @NgModule({ - declarations: [AppComponent, ChildComponent], - entryComponents: [AppComponent], + declarations: [Ng1aComponent, Ng1bComponent, Ng2Component], + entryComponents: [Ng2Component], imports: [BrowserModule, UpgradeModule] }) class Ng2Module { ngDoBootstrap() {} } - const ng1Module = angular.module('ng1', []).directive( - 'myApp', downgradeComponent({component: AppComponent})); + const ng1Module = angular.module('ng1', []) + .directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'})) + .directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'})) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run(($rootScope: angular.IRootScopeService) => { + $rootScope.l = l; + $rootScope.reset = () => log.length = 0; + }); + const element = + html('
{{reset(); l(\'1A\');}}{{l(\'1B\')}}{{l(\'1C\')}}
'); bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { - appComponent.value = 5; + expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;'); + expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); }); })); + it('should propagate changes to a downgraded component inside the ngZone', async(() => { + const element = html(''); + let appComponent: AppComponent; + + @Component({selector: 'my-app', template: ''}) + class AppComponent { + value?: number; + constructor() { appComponent = this; } + } + + @Component({ + selector: 'my-child', + template: '
{{ valueFromPromise }}
', + }) + class ChildComponent { + valueFromPromise?: number; + @Input() + set value(v: number) { expect(NgZone.isInAngularZone()).toBe(true); } + + constructor(private zone: NgZone) {} + + ngOnChanges(changes: SimpleChanges) { + if (changes['value'].isFirstChange()) return; + + this.zone.onMicrotaskEmpty.subscribe( + () => { expect(element.textContent).toEqual('5'); }); + + // Create a micro-task to update the value to be rendered asynchronously. + Promise.resolve().then(() => this.valueFromPromise = changes['value'].currentValue); + } + } + + @NgModule({ + declarations: [AppComponent, ChildComponent], + entryComponents: [AppComponent], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = angular.module('ng1', []).directive( + 'myApp', downgradeComponent({component: AppComponent})); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + appComponent.value = 5; + }); + })); + // This test demonstrates https://github.com/angular/angular/issues/6385 // which was invalidly fixed by https://github.com/angular/angular/pull/6386 // it('should not trigger $digest from an async operation in a watcher', async(() => {