fix(ivy): markForCheck() should not schedule change detection (#28048)
Previously, we had the logic to schedule a change detection tick inside markViewDirty(). This is fine when used in markDirty(), the user-facing API, because it should always schedule change detection. However, this doesn't work when used in markForCheck() because historically markForCheck() does not trigger change detection. To be backwards compatible, this commit moves the scheduling logic out of markViewDirty() and into markDirty(), so markForCheck no longer triggers a tick. PR Close #28048
This commit is contained in:
parent
feebe03523
commit
ad6569c744
@ -2376,17 +2376,24 @@ function wrapListenerWithPreventDefault(listenerFn: (e?: any) => any): EventList
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Marks current view and all ancestors dirty */
|
/**
|
||||||
export function markViewDirty(lView: LView): void {
|
* Marks current view and all ancestors dirty.
|
||||||
|
*
|
||||||
|
* Returns the root view because it is found as a byproduct of marking the view tree
|
||||||
|
* dirty, and can be used by methods that consume markViewDirty() to easily schedule
|
||||||
|
* change detection. Otherwise, such methods would need to traverse up the view tree
|
||||||
|
* an additional time to get the root view and schedule a tick on it.
|
||||||
|
*
|
||||||
|
* @param lView The starting LView to mark dirty
|
||||||
|
* @returns the root LView
|
||||||
|
*/
|
||||||
|
export function markViewDirty(lView: LView): LView {
|
||||||
while (lView && !(lView[FLAGS] & LViewFlags.IsRoot)) {
|
while (lView && !(lView[FLAGS] & LViewFlags.IsRoot)) {
|
||||||
lView[FLAGS] |= LViewFlags.Dirty;
|
lView[FLAGS] |= LViewFlags.Dirty;
|
||||||
lView = lView[PARENT] !;
|
lView = lView[PARENT] !;
|
||||||
}
|
}
|
||||||
lView[FLAGS] |= LViewFlags.Dirty;
|
lView[FLAGS] |= LViewFlags.Dirty;
|
||||||
ngDevMode && assertDefined(lView[CONTEXT], 'rootContext should be defined');
|
return lView;
|
||||||
|
|
||||||
const rootContext = lView[CONTEXT] as RootContext;
|
|
||||||
scheduleTick(rootContext, RootContextFlags.DetectChanges);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2575,7 +2582,10 @@ function updateViewQuery<T>(viewQuery: ComponentQuery<{}>| null, view: LView, co
|
|||||||
*/
|
*/
|
||||||
export function markDirty<T>(component: T) {
|
export function markDirty<T>(component: T) {
|
||||||
ngDevMode && assertDefined(component, 'component');
|
ngDevMode && assertDefined(component, 'component');
|
||||||
markViewDirty(getComponentViewByInstance(component));
|
const rootView = markViewDirty(getComponentViewByInstance(component));
|
||||||
|
|
||||||
|
ngDevMode && assertDefined(rootView[CONTEXT], 'rootContext should be defined');
|
||||||
|
scheduleTick(rootView[CONTEXT] as RootContext, RootContextFlags.DetectChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
|
@ -946,48 +946,50 @@ describe('change detection', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should schedule check on OnPush components', () => {
|
it('should ensure OnPush components are checked', () => {
|
||||||
const parent = renderComponent(OnPushParent);
|
const fixture = new ComponentFixture(OnPushParent);
|
||||||
expect(getRenderedText(parent)).toEqual('one - one');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
comp.value = 'two';
|
comp.value = 'two';
|
||||||
tick(parent);
|
tick(fixture.component);
|
||||||
expect(getRenderedText(parent)).toEqual('one - one');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
comp.cdr.markForCheck();
|
comp.cdr.markForCheck();
|
||||||
requestAnimationFrame.flush();
|
|
||||||
expect(getRenderedText(parent)).toEqual('one - two');
|
// Change detection should not have run yet, since markForCheck
|
||||||
|
// does not itself schedule change detection.
|
||||||
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
|
tick(fixture.component);
|
||||||
|
expect(fixture.hostElement.textContent).toEqual('one - two');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only run change detection once with multiple calls to markForCheck', () => {
|
it('should never schedule change detection on its own', () => {
|
||||||
renderComponent(OnPushParent);
|
const fixture = new ComponentFixture(OnPushParent);
|
||||||
expect(comp.doCheckCount).toEqual(1);
|
expect(comp.doCheckCount).toEqual(1);
|
||||||
|
|
||||||
comp.cdr.markForCheck();
|
|
||||||
comp.cdr.markForCheck();
|
|
||||||
comp.cdr.markForCheck();
|
|
||||||
comp.cdr.markForCheck();
|
comp.cdr.markForCheck();
|
||||||
comp.cdr.markForCheck();
|
comp.cdr.markForCheck();
|
||||||
requestAnimationFrame.flush();
|
requestAnimationFrame.flush();
|
||||||
|
|
||||||
expect(comp.doCheckCount).toEqual(2);
|
expect(comp.doCheckCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should schedule check on ancestor OnPush components', () => {
|
it('should ensure ancestor OnPush components are checked', () => {
|
||||||
const parent = renderComponent(OnPushParent);
|
const fixture = new ComponentFixture(OnPushParent);
|
||||||
expect(getRenderedText(parent)).toEqual('one - one');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
parent.value = 'two';
|
fixture.component.value = 'two';
|
||||||
tick(parent);
|
tick(fixture.component);
|
||||||
expect(getRenderedText(parent)).toEqual('one - one');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
comp.cdr.markForCheck();
|
comp.cdr.markForCheck();
|
||||||
requestAnimationFrame.flush();
|
tick(fixture.component);
|
||||||
expect(getRenderedText(parent)).toEqual('two - one');
|
expect(fixture.hostElement.textContent).toEqual('two - one');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should schedule check on OnPush components in embedded views', () => {
|
it('should ensure OnPush components in embedded views are checked', () => {
|
||||||
class EmbeddedViewParent {
|
class EmbeddedViewParent {
|
||||||
value = 'one';
|
value = 'one';
|
||||||
showing = true;
|
showing = true;
|
||||||
@ -1029,24 +1031,27 @@ describe('change detection', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = renderComponent(EmbeddedViewParent);
|
const fixture = new ComponentFixture(EmbeddedViewParent);
|
||||||
expect(getRenderedText(parent)).toEqual('one - one');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
comp.value = 'two';
|
comp.value = 'two';
|
||||||
tick(parent);
|
tick(fixture.component);
|
||||||
expect(getRenderedText(parent)).toEqual('one - one');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
comp.cdr.markForCheck();
|
comp.cdr.markForCheck();
|
||||||
requestAnimationFrame.flush();
|
// markForCheck should not trigger change detection on its own.
|
||||||
expect(getRenderedText(parent)).toEqual('one - two');
|
expect(fixture.hostElement.textContent).toEqual('one - one');
|
||||||
|
|
||||||
parent.value = 'two';
|
tick(fixture.component);
|
||||||
tick(parent);
|
expect(fixture.hostElement.textContent).toEqual('one - two');
|
||||||
expect(getRenderedText(parent)).toEqual('one - two');
|
|
||||||
|
fixture.component.value = 'two';
|
||||||
|
tick(fixture.component);
|
||||||
|
expect(fixture.hostElement.textContent).toEqual('one - two');
|
||||||
|
|
||||||
comp.cdr.markForCheck();
|
comp.cdr.markForCheck();
|
||||||
requestAnimationFrame.flush();
|
tick(fixture.component);
|
||||||
expect(getRenderedText(parent)).toEqual('two - two');
|
expect(fixture.hostElement.textContent).toEqual('two - two');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(kara): add test for dynamic views once bug fix is in
|
// TODO(kara): add test for dynamic views once bug fix is in
|
||||||
|
@ -451,55 +451,53 @@ describe('Integration', () => {
|
|||||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should work when an outlet is in an ngIf',
|
||||||
.it('should work when an outlet is in an ngIf',
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
const fixture = createRoot(router, RootCmp);
|
||||||
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');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should work when an outlet is added/removed', fakeAsync(() => {
|
||||||
.it('should work when an outlet is added/removed', fakeAsync(() => {
|
@Component({
|
||||||
@Component({
|
selector: 'someRoot',
|
||||||
selector: 'someRoot',
|
template: `[<div *ngIf="cond"><router-outlet></router-outlet></div>]`
|
||||||
template: `[<div *ngIf="cond"><router-outlet></router-outlet></div>]`
|
})
|
||||||
})
|
class RootCmpWithLink {
|
||||||
class RootCmpWithLink {
|
cond: boolean = true;
|
||||||
cond: boolean = true;
|
}
|
||||||
}
|
TestBed.configureTestingModule({declarations: [RootCmpWithLink]});
|
||||||
TestBed.configureTestingModule({declarations: [RootCmpWithLink]});
|
|
||||||
|
|
||||||
const router: Router = TestBed.get(Router);
|
const router: Router = TestBed.get(Router);
|
||||||
|
|
||||||
const fixture = createRoot(router, RootCmpWithLink);
|
const fixture = createRoot(router, RootCmpWithLink);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{path: 'simple', component: SimpleCmp},
|
{path: 'simple', component: SimpleCmp},
|
||||||
{path: 'blank', component: BlankCmp},
|
{path: 'blank', component: BlankCmp},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
router.navigateByUrl('/simple');
|
router.navigateByUrl('/simple');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.nativeElement).toHaveText('[simple]');
|
expect(fixture.nativeElement).toHaveText('[simple]');
|
||||||
|
|
||||||
fixture.componentInstance.cond = false;
|
fixture.componentInstance.cond = false;
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.nativeElement).toHaveText('[]');
|
expect(fixture.nativeElement).toHaveText('[]');
|
||||||
|
|
||||||
fixture.componentInstance.cond = true;
|
fixture.componentInstance.cond = true;
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.nativeElement).toHaveText('[simple]');
|
expect(fixture.nativeElement).toHaveText('[simple]');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should update location when navigating', fakeAsync(() => {
|
it('should update location when navigating', fakeAsync(() => {
|
||||||
@Component({template: `record`})
|
@Component({template: `record`})
|
||||||
@ -633,49 +631,46 @@ describe('Integration', () => {
|
|||||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should navigate back and forward',
|
||||||
.it('should navigate back and forward',
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
const fixture = createRoot(router, RootCmp);
|
||||||
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: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}]
|
||||||
{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) => {
|
||||||
@ -1006,36 +1001,34 @@ describe('Integration', () => {
|
|||||||
]);
|
]);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should handle failed navigations gracefully', fakeAsync(inject([Router], (router: Router) => {
|
||||||
.it('should handle failed navigations gracefully',
|
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));
|
||||||
|
|
||||||
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) => {
|
||||||
@ -1883,50 +1876,45 @@ describe('Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('redirects', () => {
|
describe('redirects', () => {
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should work', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
.it('should work',
|
const fixture = createRoot(router, RootCmp);
|
||||||
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: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp}
|
||||||
{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');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should update Navigation object after redirects are applied',
|
||||||
.it('should update Navigation object after redirects are applied',
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
const fixture = createRoot(router, RootCmp);
|
||||||
const fixture = createRoot(router, RootCmp);
|
let initialUrl, afterRedirectUrl;
|
||||||
let initialUrl, afterRedirectUrl;
|
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{path: 'old/team/:id', redirectTo: 'team/:id'},
|
{path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp}
|
||||||
{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) => {
|
||||||
@ -2031,18 +2019,16 @@ describe('Integration', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
.it('works',
|
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', 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', () => {
|
||||||
@ -2054,19 +2040,17 @@ describe('Integration', () => {
|
|||||||
|
|
||||||
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
|
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
.it('works',
|
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', 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', () => {
|
||||||
@ -3355,38 +3339,37 @@ describe('Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('route events', () => {
|
describe('route events', () => {
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should fire matching (Child)ActivationStart/End events',
|
||||||
.it('should fire matching (Child)ActivationStart/End events',
|
fakeAsync(inject([Router], (router: Router) => {
|
||||||
fakeAsync(inject([Router], (router: Router) => {
|
const fixture = createRoot(router, RootCmp);
|
||||||
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) => {
|
||||||
@ -4528,82 +4511,81 @@ describe('Integration', () => {
|
|||||||
expect(simpleCmp1).not.toBe(simpleCmp2);
|
expect(simpleCmp1).not.toBe(simpleCmp2);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
fixmeIvy('FW-768: markViewDirty instruction is scheduling a tick')
|
it('should not mount the component of the previously reused route when the outlet was not instantiated at the time of route activation',
|
||||||
.it('should not mount the component of the previously reused route when the outlet was not instantiated at the time of route activation',
|
fakeAsync(() => {
|
||||||
fakeAsync(() => {
|
@Component({
|
||||||
@Component({
|
selector: 'root-cmp',
|
||||||
selector: 'root-cmp',
|
template:
|
||||||
template:
|
'<div *ngIf="isToolpanelShowing"><router-outlet name="toolpanel"></router-outlet></div>'
|
||||||
'<div *ngIf="isToolpanelShowing"><router-outlet name="toolpanel"></router-outlet></div>'
|
})
|
||||||
})
|
class RootCmpWithCondOutlet implements OnDestroy {
|
||||||
class RootCmpWithCondOutlet implements OnDestroy {
|
private subscription: Subscription;
|
||||||
private subscription: Subscription;
|
public isToolpanelShowing: boolean = false;
|
||||||
public isToolpanelShowing: boolean = false;
|
|
||||||
|
|
||||||
constructor(router: Router) {
|
constructor(router: Router) {
|
||||||
this.subscription =
|
this.subscription =
|
||||||
router.events.pipe(filter(event => event instanceof NavigationEnd))
|
router.events.pipe(filter(event => event instanceof NavigationEnd))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => this.isToolpanelShowing =
|
() => this.isToolpanelShowing =
|
||||||
!!router.parseUrl(router.url).root.children['toolpanel']);
|
!!router.parseUrl(router.url).root.children['toolpanel']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy(): void { this.subscription.unsubscribe(); }
|
public ngOnDestroy(): void { this.subscription.unsubscribe(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'tool-1-cmp', template: 'Tool 1 showing'})
|
@Component({selector: 'tool-1-cmp', template: 'Tool 1 showing'})
|
||||||
class Tool1Component {
|
class Tool1Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'tool-2-cmp', template: 'Tool 2 showing'})
|
@Component({selector: 'tool-2-cmp', template: 'Tool 2 showing'})
|
||||||
class Tool2Component {
|
class Tool2Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RootCmpWithCondOutlet, Tool1Component, Tool2Component],
|
declarations: [RootCmpWithCondOutlet, Tool1Component, Tool2Component],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterTestingModule.withRoutes([
|
RouterTestingModule.withRoutes([
|
||||||
{path: 'a', outlet: 'toolpanel', component: Tool1Component},
|
{path: 'a', outlet: 'toolpanel', component: Tool1Component},
|
||||||
{path: 'b', outlet: 'toolpanel', component: Tool2Component},
|
{path: 'b', outlet: 'toolpanel', component: Tool2Component},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
class TestModule {
|
class TestModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
TestBed.configureTestingModule({imports: [TestModule]});
|
TestBed.configureTestingModule({imports: [TestModule]});
|
||||||
|
|
||||||
const router: Router = TestBed.get(Router);
|
const router: Router = TestBed.get(Router);
|
||||||
router.routeReuseStrategy = new AttachDetachReuseStrategy();
|
router.routeReuseStrategy = new AttachDetachReuseStrategy();
|
||||||
|
|
||||||
const fixture = createRoot(router, RootCmpWithCondOutlet);
|
const fixture = createRoot(router, RootCmpWithCondOutlet);
|
||||||
|
|
||||||
// Activate 'tool-1'
|
// Activate 'tool-1'
|
||||||
router.navigate([{outlets: {toolpanel: 'a'}}]);
|
router.navigate([{outlets: {toolpanel: 'a'}}]);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture).toContainComponent(Tool1Component, '(a)');
|
expect(fixture).toContainComponent(Tool1Component, '(a)');
|
||||||
|
|
||||||
// Deactivate 'tool-1'
|
// Deactivate 'tool-1'
|
||||||
router.navigate([{outlets: {toolpanel: null}}]);
|
router.navigate([{outlets: {toolpanel: null}}]);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture).not.toContainComponent(Tool1Component, '(b)');
|
expect(fixture).not.toContainComponent(Tool1Component, '(b)');
|
||||||
|
|
||||||
// Activate 'tool-1'
|
// Activate 'tool-1'
|
||||||
router.navigate([{outlets: {toolpanel: 'a'}}]);
|
router.navigate([{outlets: {toolpanel: 'a'}}]);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture).toContainComponent(Tool1Component, '(c)');
|
expect(fixture).toContainComponent(Tool1Component, '(c)');
|
||||||
|
|
||||||
// Deactivate 'tool-1'
|
// Deactivate 'tool-1'
|
||||||
router.navigate([{outlets: {toolpanel: null}}]);
|
router.navigate([{outlets: {toolpanel: null}}]);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture).not.toContainComponent(Tool1Component, '(d)');
|
expect(fixture).not.toContainComponent(Tool1Component, '(d)');
|
||||||
|
|
||||||
// Activate 'tool-2'
|
// Activate 'tool-2'
|
||||||
router.navigate([{outlets: {toolpanel: 'b'}}]);
|
router.navigate([{outlets: {toolpanel: 'b'}}]);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture).toContainComponent(Tool2Component, '(e)');
|
expect(fixture).toContainComponent(Tool2Component, '(e)');
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user