fix(ivy): use NgZone.onStable when bootstraped using PlatformRef (#27898)

PR Close #27898
This commit is contained in:
Miško Hevery 2019-01-02 15:12:36 -08:00 committed by Kara Erickson
parent 1a7f92c423
commit b9c6df6da7
5 changed files with 349 additions and 324 deletions

View File

@ -3,7 +3,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime": 1497, "runtime": 1497,
"main": 187112, "main": 187134,
"polyfills": 59608 "polyfills": 59608
} }
} }

View File

@ -11,13 +11,14 @@ import {ApplicationRef} from './application_ref';
import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens';
import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection';
import {Console} from './console'; import {Console} from './console';
import {InjectionToken, Injector, StaticProvider} from './di'; import {Injector, StaticProvider} from './di';
import {Inject, Optional, SkipSelf} from './di/metadata'; import {Inject, Optional, SkipSelf} from './di/metadata';
import {ErrorHandler} from './error_handler'; import {ErrorHandler} from './error_handler';
import {LOCALE_ID} from './i18n/tokens'; import {LOCALE_ID} from './i18n/tokens';
import {ComponentFactoryResolver} from './linker'; import {ComponentFactoryResolver} from './linker';
import {Compiler} from './linker/compiler'; import {Compiler} from './linker/compiler';
import {NgModule} from './metadata'; import {NgModule} from './metadata';
import {SCHEDULER} from './render3/component_ref';
import {NgZone} from './zone'; import {NgZone} from './zone';
export function _iterableDiffersFactory() { export function _iterableDiffersFactory() {
@ -43,6 +44,7 @@ export const APPLICATION_MODULE_PROVIDERS: StaticProvider[] = [
deps: deps:
[NgZone, Console, Injector, ErrorHandler, ComponentFactoryResolver, ApplicationInitStatus] [NgZone, Console, Injector, ErrorHandler, ComponentFactoryResolver, ApplicationInitStatus]
}, },
{provide: SCHEDULER, deps: [NgZone], useFactory: zoneSchedulerFactory},
{ {
provide: ApplicationInitStatus, provide: ApplicationInitStatus,
useClass: 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 * Configures the root injector for an app with
* providers of `@angular/core` dependencies that `ApplicationRef` needs * providers of `@angular/core` dependencies that `ApplicationRef` needs

View File

@ -451,21 +451,22 @@ describe('Integration', () => {
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
}))); })));
it('should work when an outlet is in an ngIf', fixmeIvy('unknown/maybe FW-918')
fakeAsync(inject([Router, Location], (router: Router, location: Location) => { .it('should work when an outlet is in an ngIf',
const fixture = createRoot(router, RootCmp); fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{ router.resetConfig([{
path: 'child', path: 'child',
component: OutletInNgIf, component: OutletInNgIf,
children: [{path: 'simple', component: SimpleCmp}] children: [{path: 'simple', component: SimpleCmp}]
}]); }]);
router.navigateByUrl('/child/simple'); router.navigateByUrl('/child/simple');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/child/simple'); expect(location.path()).toEqual('/child/simple');
}))); })));
it('should work when an outlet is added/removed', fakeAsync(() => { it('should work when an outlet is added/removed', fakeAsync(() => {
@Component({ @Component({
@ -634,46 +635,49 @@ describe('Integration', () => {
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
}))); })));
it('should navigate back and forward', fixmeIvy('unknown/maybe FW-918')
fakeAsync(inject([Router, Location], (router: Router, location: Location) => { .it('should navigate back and forward',
const fixture = createRoot(router, RootCmp); fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{ router.resetConfig([{
path: 'team/:id', path: 'team/:id',
component: TeamCmp, component: TeamCmp,
children: children: [
[{path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}] {path: 'simple', component: SimpleCmp},
}]); {path: 'user/:name', component: UserCmp}
]
}]);
let event: NavigationStart; let event: NavigationStart;
router.events.subscribe(e => { router.events.subscribe(e => {
if (e instanceof NavigationStart) { if (e instanceof NavigationStart) {
event = e; event = e;
} }
}); });
router.navigateByUrl('/team/33/simple'); router.navigateByUrl('/team/33/simple');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/33/simple'); expect(location.path()).toEqual('/team/33/simple');
const simpleNavStart = event !; const simpleNavStart = event !;
router.navigateByUrl('/team/22/user/victor'); router.navigateByUrl('/team/22/user/victor');
advance(fixture); advance(fixture);
const userVictorNavStart = event !; const userVictorNavStart = event !;
location.back(); location.back();
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/33/simple'); expect(location.path()).toEqual('/team/33/simple');
expect(event !.navigationTrigger).toEqual('hashchange'); expect(event !.navigationTrigger).toEqual('hashchange');
expect(event !.restoredState !.navigationId).toEqual(simpleNavStart.id); expect(event !.restoredState !.navigationId).toEqual(simpleNavStart.id);
location.forward(); location.forward();
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/22/user/victor'); expect(location.path()).toEqual('/team/22/user/victor');
expect(event !.navigationTrigger).toEqual('hashchange'); expect(event !.navigationTrigger).toEqual('hashchange');
expect(event !.restoredState !.navigationId).toEqual(userVictorNavStart.id); expect(event !.restoredState !.navigationId).toEqual(userVictorNavStart.id);
}))); })));
it('should navigate to the same url when config changes', it('should navigate to the same url when config changes',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => { 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) => { fixmeIvy('unknown/maybe FW-918')
const fixture = createRoot(router, RootCmp); .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[] = []; const recordedEvents: any[] = [];
router.events.forEach(e => recordedEvents.push(e)); router.events.forEach(e => recordedEvents.push(e));
let e: any; let e: any;
router.navigateByUrl('/invalid') !.catch(_ => e = _); router.navigateByUrl('/invalid') !.catch(_ => e = _);
advance(fixture); advance(fixture);
expect(e.message).toContain('Cannot match any routes'); expect(e.message).toContain('Cannot match any routes');
router.navigateByUrl('/user/fedor'); router.navigateByUrl('/user/fedor');
advance(fixture); advance(fixture);
expect(fixture.nativeElement).toHaveText('user fedor'); expect(fixture.nativeElement).toHaveText('user fedor');
expectEvents(recordedEvents, [ expectEvents(recordedEvents, [
[NavigationStart, '/invalid'], [NavigationError, '/invalid'], [NavigationStart, '/invalid'], [NavigationError, '/invalid'],
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart], [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart],
[GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd], [ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd],
[NavigationEnd, '/user/fedor'] [NavigationEnd, '/user/fedor']
]); ]);
}))); })));
// Errors should behave the same for both deferred and eager URL update strategies // Errors should behave the same for both deferred and eager URL update strategies
['deferred', 'eager'].forEach((strat: any) => { ['deferred', 'eager'].forEach((strat: any) => {
@ -1879,45 +1885,50 @@ describe('Integration', () => {
}); });
describe('redirects', () => { describe('redirects', () => {
it('should work', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { fixmeIvy('unkwnown/maybe FW-918')
const fixture = createRoot(router, RootCmp); .it('should work',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([ router.resetConfig([
{path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp} {path: 'old/team/:id', redirectTo: 'team/:id'},
]); {path: 'team/:id', component: TeamCmp}
]);
router.navigateByUrl('old/team/22'); router.navigateByUrl('old/team/22');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/22'); expect(location.path()).toEqual('/team/22');
}))); })));
it('should update Navigation object after redirects are applied', fixmeIvy('unkwnown/maybe FW-918')
fakeAsync(inject([Router, Location], (router: Router, location: Location) => { .it('should update Navigation object after redirects are applied',
const fixture = createRoot(router, RootCmp); fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
let initialUrl, afterRedirectUrl; const fixture = createRoot(router, RootCmp);
let initialUrl, afterRedirectUrl;
router.resetConfig([ router.resetConfig([
{path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp} {path: 'old/team/:id', redirectTo: 'team/:id'},
]); {path: 'team/:id', component: TeamCmp}
]);
router.events.subscribe(e => { router.events.subscribe(e => {
if (e instanceof NavigationStart) { if (e instanceof NavigationStart) {
const navigation = router.getCurrentNavigation(); const navigation = router.getCurrentNavigation();
initialUrl = navigation && navigation.finalUrl; initialUrl = navigation && navigation.finalUrl;
} }
if (e instanceof RoutesRecognized) { if (e instanceof RoutesRecognized) {
const navigation = router.getCurrentNavigation(); const navigation = router.getCurrentNavigation();
afterRedirectUrl = navigation && navigation.finalUrl; afterRedirectUrl = navigation && navigation.finalUrl;
} }
}); });
router.navigateByUrl('old/team/22'); router.navigateByUrl('old/team/22');
advance(fixture); advance(fixture);
expect(initialUrl).toBeUndefined(); expect(initialUrl).toBeUndefined();
expect(router.serializeUrl(afterRedirectUrl as any)).toBe('/team/22'); expect(router.serializeUrl(afterRedirectUrl as any)).toBe('/team/22');
}))); })));
it('should not break the back button when trigger by location change', it('should not break the back button when trigger by location change',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => { fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
@ -2022,17 +2033,18 @@ describe('Integration', () => {
}); });
}); });
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { fixmeIvy('unknown/maybe FW-918')
const fixture = createRoot(router, RootCmp); .it('works',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig( router.resetConfig(
[{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]); [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]);
router.navigateByUrl('/team/22'); router.navigateByUrl('/team/22');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/22');
expect(location.path()).toEqual('/team/22'); })));
})));
}); });
describe('should work when given a class', () => { describe('should work when given a class', () => {
@ -2044,17 +2056,19 @@ describe('Integration', () => {
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); }); beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { fixmeIvy('unknown/maybe FW-918')
const fixture = createRoot(router, RootCmp); .it('works',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig( router.resetConfig(
[{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]); [{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]);
router.navigateByUrl('/team/22'); router.navigateByUrl('/team/22');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/22'); expect(location.path()).toEqual('/team/22');
}))); })));
}); });
describe('should work when returns an observable', () => { describe('should work when returns an observable', () => {
@ -3343,37 +3357,38 @@ describe('Integration', () => {
}); });
describe('route events', () => { describe('route events', () => {
it('should fire matching (Child)ActivationStart/End events', fixmeIvy('unknown/maybe FW-918')
fakeAsync(inject([Router], (router: Router) => { .it('should fire matching (Child)ActivationStart/End events',
const fixture = createRoot(router, RootCmp); 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[] = []; const recordedEvents: any[] = [];
router.events.forEach(e => recordedEvents.push(e)); router.events.forEach(e => recordedEvents.push(e));
router.navigateByUrl('/user/fedor'); router.navigateByUrl('/user/fedor');
advance(fixture); advance(fixture);
expect(fixture.nativeElement).toHaveText('user fedor'); expect(fixture.nativeElement).toHaveText('user fedor');
expect(recordedEvents[3] instanceof ChildActivationStart).toBe(true); expect(recordedEvents[3] instanceof ChildActivationStart).toBe(true);
expect(recordedEvents[3].snapshot).toBe(recordedEvents[9].snapshot.root); expect(recordedEvents[3].snapshot).toBe(recordedEvents[9].snapshot.root);
expect(recordedEvents[9] instanceof ChildActivationEnd).toBe(true); expect(recordedEvents[9] instanceof ChildActivationEnd).toBe(true);
expect(recordedEvents[9].snapshot).toBe(recordedEvents[9].snapshot.root); expect(recordedEvents[9].snapshot).toBe(recordedEvents[9].snapshot.root);
expect(recordedEvents[4] instanceof ActivationStart).toBe(true); expect(recordedEvents[4] instanceof ActivationStart).toBe(true);
expect(recordedEvents[4].snapshot.routeConfig.path).toBe('user/:name'); expect(recordedEvents[4].snapshot.routeConfig.path).toBe('user/:name');
expect(recordedEvents[8] instanceof ActivationEnd).toBe(true); expect(recordedEvents[8] instanceof ActivationEnd).toBe(true);
expect(recordedEvents[8].snapshot.routeConfig.path).toBe('user/:name'); expect(recordedEvents[8].snapshot.routeConfig.path).toBe('user/:name');
expectEvents(recordedEvents, [ expectEvents(recordedEvents, [
[NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'], [NavigationStart, '/user/fedor'], [RoutesRecognized, '/user/fedor'],
[GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart], [GuardsCheckStart, '/user/fedor'], [ChildActivationStart], [ActivationStart],
[GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'], [GuardsCheckEnd, '/user/fedor'], [ResolveStart, '/user/fedor'],
[ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd], [ResolveEnd, '/user/fedor'], [ActivationEnd], [ChildActivationEnd],
[NavigationEnd, '/user/fedor'] [NavigationEnd, '/user/fedor']
]); ]);
}))); })));
it('should allow redirection in NavigationStart', it('should allow redirection in NavigationStart',
fakeAsync(inject([Router], (router: Router) => { fakeAsync(inject([Router], (router: Router) => {

View File

@ -170,110 +170,104 @@ withEachNg1Version(() => {
}); });
describe('scope/component change-detection', () => { 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')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{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('<div>{{reset(); l(\'1A\');}}<ng2>{{l(\'1B\')}}</ng2>{{l(\'1C\')}}</div>');
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( fixmeIvy(
'FW-712: Rendering is being run on next "animation frame" rather than "Zone.microTaskEmpty" trigger') 'FW-918: Create API and mental model to work with Host Element; and ChangeDetections')
.it('should propagate changes to a downgraded component inside the ngZone', async(() => { .it('should interleave scope and component expressions', async(() => {
let appComponent: AppComponent; const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
let upgradeRef: UpgradeAdapterRef; const ng1Module = angular.module('ng1', []);
const log: string[] = [];
const l = (value: string) => {
log.push(value);
return value + ';';
};
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'}) ng1Module.directive('ng1a', () => ({template: '{{ l(\'ng1a\') }}'}));
class AppComponent { ng1Module.directive('ng1b', () => ({template: '{{ l(\'ng1b\') }}'}));
value?: number; ng1Module.run(($rootScope: any) => {
constructor() { appComponent = this; } $rootScope.l = l;
} $rootScope.reset = () => log.length = 0;
});
@Component({ @Component({
selector: 'my-child', selector: 'ng2',
template: '<div>{{valueFromPromise}}', template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`
}) })
class ChildComponent { class Ng2 {
valueFromPromise?: number; l: any;
@Input() constructor() { this.l = l; }
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);
}
} }
@NgModule({declarations: [AppComponent, ChildComponent], imports: [BrowserModule]}) @NgModule({
declarations: [
adapter.upgradeNg1Component('ng1a'), adapter.upgradeNg1Component('ng1b'), Ng2
],
imports: [BrowserModule],
})
class Ng2Module { class Ng2Module {
} }
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
const ng1Module = angular.module('ng1', []).directive(
'myApp', adapter.downgradeNg2Component(AppComponent));
const element = html('<my-app></my-app>');
const element =
html('<div>{{reset(); l(\'1A\');}}<ng2>{{l(\'1B\')}}</ng2>{{l(\'1C\')}}</div>');
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
upgradeRef = ref; expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;');
appComponent.value = 5; // 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: '<my-child [value]="value"></my-child>'})
class AppComponent {
value?: number;
constructor() { appComponent = this; }
}
@Component({
selector: 'my-child',
template: '<div>{{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('<my-app></my-app>');
adapter.bootstrap(element, ['ng1']).ready((ref) => {
upgradeRef = ref;
appComponent.value = 5;
});
}));
// This test demonstrates https://github.com/angular/angular/issues/6385 // This test demonstrates https://github.com/angular/angular/issues/6385
// which was invalidly fixed by https://github.com/angular/angular/pull/6386 // 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(() => { // it('should not trigger $digest from an async operation in a watcher', async(() => {

View File

@ -21,116 +21,111 @@ withEachNg1Version(() => {
beforeEach(() => destroyPlatform()); beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform()); afterEach(() => destroyPlatform());
it('should interleave scope and component expressions', async(() => { fixmeIvy('FW-918: Create API and mental model to work with Host Element; and ChangeDetections')
const log: string[] = []; .it('should interleave scope and component expressions', async(() => {
const l = (value: string) => { const log: string[] = [];
log.push(value); const l = (value: string) => {
return value + ';'; log.push(value);
}; return value + ';';
};
@Directive({selector: 'ng1a'}) @Directive({selector: 'ng1a'})
class Ng1aComponent extends UpgradeComponent { class Ng1aComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) { constructor(elementRef: ElementRef, injector: Injector) {
super('ng1a', elementRef, 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')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{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('<div>{{reset(); l(\'1A\');}}<ng2>{{l(\'1B\')}}</ng2>{{l(\'1C\')}}</div>');
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('<my-app></my-app>');
let appComponent: AppComponent;
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'})
class AppComponent {
value?: number;
constructor() { appComponent = this; }
}
@Component({
selector: 'my-child',
template: '<div>{{ valueFromPromise }}</div>',
})
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: 'ng1b'})
class Ng1bComponent extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1b', elementRef, injector);
}
}
@Component({
selector: 'ng2',
template: `{{l('2A')}}<ng1a></ng1a>{{l('2B')}}<ng1b></ng1b>{{l('2C')}}`
})
class Ng2Component {
l = l;
}
@NgModule({ @NgModule({
declarations: [AppComponent, ChildComponent], declarations: [Ng1aComponent, Ng1bComponent, Ng2Component],
entryComponents: [AppComponent], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', [])
'myApp', downgradeComponent({component: AppComponent})); .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('<div>{{reset(); l(\'1A\');}}<ng2>{{l(\'1B\')}}</ng2>{{l(\'1C\')}}</div>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { 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('<my-app></my-app>');
let appComponent: AppComponent;
@Component({selector: 'my-app', template: '<my-child [value]="value"></my-child>'})
class AppComponent {
value?: number;
constructor() { appComponent = this; }
}
@Component({
selector: 'my-child',
template: '<div>{{ valueFromPromise }}</div>',
})
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 // This test demonstrates https://github.com/angular/angular/issues/6385
// which was invalidly fixed by https://github.com/angular/angular/pull/6386 // 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(() => { // it('should not trigger $digest from an async operation in a watcher', async(() => {