fix(router): mount correct component if router outlet was not instantiated and if using a route reuse strategy (#25313) (#25314)

This unsets 'attachRef' on outlet context if no route is to be reused in route activation.

Closes #25313

PR Close #25314
This commit is contained in:
Daniel Wiehl 2018-08-05 15:35:51 +02:00 committed by Igor Minar
parent f2ba55f2fb
commit 8dc2b119fb
5 changed files with 113 additions and 5 deletions

View File

@ -10,6 +10,7 @@ ng_module(
module_name = "@angular/platform-browser/testing",
deps = [
"//packages/core",
"//packages/core/testing",
"//packages/platform-browser",
"@rxjs",
],

View File

@ -11,6 +11,7 @@ const sourcemaps = require('rollup-plugin-sourcemaps');
const globals = {
'@angular/core': 'ng.core',
'@angular/core/testing': 'ng.core.testing',
'@angular/common': 'ng.common',
'@angular/platform-browser': 'ng.platformBrowser'
};

View File

@ -7,8 +7,9 @@
*/
import {ɵglobal as global} from '@angular/core';
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
import {Type, ɵglobal as global} from '@angular/core';
import {ComponentFixture} from '@angular/core/testing';
import {By, ɵgetDOM as getDOM} from '@angular/platform-browser';
@ -79,6 +80,11 @@ export interface NgMatchers<T = any> extends jasmine.Matchers<T> {
*/
toContainError(expected: any): boolean;
/**
* Expect a component of the given type to show.
*/
toContainComponent(expectedComponentType: Type<any>, expectationFailOutput?: any): boolean;
/**
* Invert the matchers.
*/
@ -235,6 +241,29 @@ _global.beforeEach(function() {
};
}
};
},
toContainComponent: function() {
return {
compare: function(actualFixture: any, expectedComponentType: Type<any>) {
const failOutput = arguments[2];
const msgFn = (msg: string): string => [msg, failOutput].filter(Boolean).join(', ');
// verify correct actual type
if (!(actualFixture instanceof ComponentFixture)) {
return {
pass: false,
message: msgFn(
`Expected actual to be of type \'ComponentFixture\' [actual=${actualFixture.constructor.name}]`)
};
}
const found = !!actualFixture.debugElement.query(By.directive(expectedComponentType));
return found ?
{pass: true} :
{pass: false, message: msgFn(`Expected ${expectedComponentType.name} to show`)};
}
};
}
});
});

View File

@ -1043,6 +1043,7 @@ class ActivateRoutes {
const config = parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;
context.attachRef = null;
context.route = future;
context.resolver = cmpFactoryResolver;
if (context.outlet) {

View File

@ -8,13 +8,13 @@
import {CommonModule, Location} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, OnDestroy, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
import {Observable, Observer, of } from 'rxjs';
import {map} from 'rxjs/operators';
import {Observable, Observer, Subscription, of } from 'rxjs';
import {filter, map} from 'rxjs/operators';
import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -4022,6 +4022,82 @@ describe('Integration', () => {
const simpleCmp2 = fixture.debugElement.children[1].componentInstance;
expect(simpleCmp1).not.toBe(simpleCmp2);
})));
it('should not mount the component of the previously reused route when the outlet was not instantiated at the time of route activation',
fakeAsync(() => {
@Component({
selector: 'root-cmp',
template:
'<div *ngIf="isToolpanelShowing"><router-outlet name="toolpanel"></router-outlet></div>'
})
class RootCmpWithCondOutlet implements OnDestroy {
private subscription: Subscription;
public isToolpanelShowing: boolean = false;
constructor(router: Router) {
this.subscription =
router.events.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(
() => this.isToolpanelShowing =
!!router.parseUrl(router.url).root.children['toolpanel']);
}
public ngOnDestroy(): void { this.subscription.unsubscribe(); }
}
@Component({selector: 'tool-1-cmp', template: 'Tool 1 showing'})
class Tool1Component {
}
@Component({selector: 'tool-2-cmp', template: 'Tool 2 showing'})
class Tool2Component {
}
@NgModule({
declarations: [RootCmpWithCondOutlet, Tool1Component, Tool2Component],
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{path: 'a', outlet: 'toolpanel', component: Tool1Component},
{path: 'b', outlet: 'toolpanel', component: Tool2Component},
]),
],
})
class TestModule {
}
TestBed.configureTestingModule({imports: [TestModule]});
const router: Router = TestBed.get(Router);
router.routeReuseStrategy = new AttachDetachReuseStrategy();
const fixture = createRoot(router, RootCmpWithCondOutlet);
// Activate 'tool-1'
router.navigate([{outlets: {toolpanel: 'a'}}]);
advance(fixture);
expect(fixture).toContainComponent(Tool1Component, '(a)');
// Deactivate 'tool-1'
router.navigate([{outlets: {toolpanel: null}}]);
advance(fixture);
expect(fixture).not.toContainComponent(Tool1Component, '(b)');
// Activate 'tool-1'
router.navigate([{outlets: {toolpanel: 'a'}}]);
advance(fixture);
expect(fixture).toContainComponent(Tool1Component, '(c)');
// Deactivate 'tool-1'
router.navigate([{outlets: {toolpanel: null}}]);
advance(fixture);
expect(fixture).not.toContainComponent(Tool1Component, '(d)');
// Activate 'tool-2'
router.navigate([{outlets: {toolpanel: 'b'}}]);
advance(fixture);
expect(fixture).toContainComponent(Tool2Component, '(e)');
}));
});
});