-
-
Save LayZeeDK/3e80b507b31089da1bc9b5db07c1519f to your computer and use it in GitHub Desktop.
<div *ngIf="hero"> | |
<h2> | |
{{hero.name | uppercase}} Details | |
</h2> | |
<div> | |
<span>id:</span> | |
{{hero.id}} | |
</div> | |
<div> | |
<label> | |
name: | |
<input [(ngModel)]="hero.name" placeholder="name"> | |
</label> | |
</div> | |
<button (click)="goBack()">go back</button> | |
<button (click)="save()">save</button> | |
</div> |
import { Component } from '@angular/core'; | |
import { | |
ComponentFixture, | |
fakeAsync, | |
TestBed, | |
tick, | |
} from '@angular/core/testing'; | |
import { FormsModule } from '@angular/forms'; | |
import { By } from '@angular/platform-browser'; | |
import { Router } from '@angular/router'; | |
import { RouterTestingModule } from '@angular/router/testing'; | |
import { of } from 'rxjs'; | |
import { Hero } from '../hero'; | |
import { HeroService } from '../hero.service'; | |
import { HEROES } from '../mock-heroes'; | |
import { HeroDetailComponent } from './hero-detail.component'; | |
@Component({ | |
template: '<router-outlet></router-outlet>', | |
}) | |
class TestRootComponent {} | |
describe('HeroDetailComponent (integrated)', () => { | |
function advance() { | |
tick(); | |
rootFixture.detectChanges(); | |
} | |
function getTitle() { | |
const element = rootFixture.debugElement.query(By.css('h2')) | |
.nativeElement as HTMLElement; | |
return element.textContent.trim(); | |
} | |
function navigateByHeroId(id: number) { | |
rootFixture.ngZone.run(() => router.navigate(['detail', id])); | |
} | |
beforeEach(async () => { | |
const fakeService = { | |
getHero(id: number) { | |
const hero = [...fakeHeroes].find(h => h.id === id); | |
return of(hero); | |
}, | |
} as Partial<HeroService>; | |
TestBed.configureTestingModule({ | |
declarations: [ | |
TestRootComponent, | |
HeroDetailComponent, | |
], | |
imports: [ | |
RouterTestingModule.withRoutes([ | |
{ path: 'detail/:id', component: HeroDetailComponent }, | |
]), | |
FormsModule, | |
], | |
providers: [ | |
{ provide: HeroService, useValue: fakeService }, | |
], | |
}); | |
await TestBed.compileComponents(); | |
rootFixture = TestBed.createComponent(TestRootComponent); | |
router = TestBed.inject(Router); | |
}); | |
const fakeHeroes: ReadonlyArray<Hero> = [...HEROES]; | |
let router: Router; | |
let rootFixture: ComponentFixture<TestRootComponent>; | |
it("displays the hero's name in upper-case letters", fakeAsync(() => { | |
const [expectedHero] = fakeHeroes; | |
navigateByHeroId(expectedHero.id); | |
advance(); | |
expect(getTitle()).toContain(expectedHero.name.toUpperCase()); | |
})); | |
}); |
import { Location } from '@angular/common'; | |
import { Component, Input, OnInit } from '@angular/core'; | |
import { ActivatedRoute } from '@angular/router'; | |
import { Hero } from '../hero'; | |
import { HeroService } from '../hero.service'; | |
@Component({ | |
selector: 'app-hero-detail', | |
styleUrls: ['./hero-detail.component.css'], | |
templateUrl: './hero-detail.component.html', | |
}) | |
export class HeroDetailComponent implements OnInit { | |
@Input() hero: Hero; | |
constructor( | |
private route: ActivatedRoute, | |
private heroService: HeroService, | |
private location: Location, | |
) {} | |
ngOnInit(): void { | |
this.getHero(); | |
} | |
getHero(): void { | |
const id = +this.route.snapshot.paramMap.get('id'); | |
this.heroService.getHero(id) | |
.subscribe(hero => this.hero = hero); | |
} | |
goBack(): void { | |
this.location.back(); | |
} | |
save(): void { | |
this.heroService.updateHero(this.hero) | |
.subscribe(() => this.goBack()); | |
} | |
} |
Hi @Ruud-cb,
Thank you for your suggestion! The article that this Gist is related to (Testing routed Angular components with the RouterTestingModule) focuses on routed components.
The hero detail component is special since it's both a routed component (it's the target of a route configuration) and a routing component (it triggers navigation to other routes).
In Testing Angular routing components with the RouterTestingModule, we test routing components. We can use those techniques to create the test case, you're asking for.
However, this test case is a little special. We start in some component (which one doesn't really matter for the sake of a test isolated to the hero detail component), then we navigate to the hero detail component, then we press the go back button to trigger a back navigation command. Finally, we assert the value of the URL and title which should match the initial ones.
We're going to need Title#getTitle and Location#path for those assertions.
We'll need a routing configuration like this:
import { OnDestroy, OnInit, TestBed } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Title } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
@Component({
template: '<router-outlet></router-outlet>',
})
class TestRootComponent {}
@Component({
template: '<p>TestSourceComponent</p>',
})
class TestSourceComponent implements OnDestroy, OnInit {
constructor(
private title: Title,
) {}
ngOnInit(): void {
this.title.setTitle('TestSourceComponent')
}
ngOnDestroy(): void {
this.title.setTitle('')
}
}
describe('HeroDetailComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
TestRootComponent,
TestSourceComponent,
],
imports: [
RouterTestingModule.withRoutes([
{ path: 'test-source', component: TestSourceComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: '', pathMatch: 'full', redirectTo: 'test-source' },
]),
],
});
});
});
Do you want to give the rest a try yourself?
Hi @LayZeeDK, I have similar question:
I use jasmine test to navigation between components, for example: click link, wait for page to change and assert information on the page.
routeLink
or router.navigateByUrl(..)
works in production and jasmine tests. However when i'm going: this.location.back();
it only works in production, in jasmine test it doesn't navigate back.
Is there a way to use location.back()
and have router change page in jasmine test?
Thank you!
Hi @meirka
If you're using Karma+Jasmine, importing the RouterTestingModule
prevents Angular's Location
and Router
services from changing the actual URL of the browser controlled by Karma. However, the component in your test component's router outlet should change.
Can you take a look at my simple example? I created it just to make sure we are on the same page:
https://github.com/meirka/angular-unit-test-navigation
It looks to me that Location actually changes pages (via angular routing)... lol. I wonder what broke in my actual project, since location.back() doesn't seem to work there :(
@meirka
I identified a few issues and suggested some stylistic changes to your repo: meirka/angular-unit-test-navigation#1
@LayZeeDK
Thank you, I'll go over your changes.
I'm attempting to do kinda full integration test: navigation, fill forms, backend calls (mocked by testingController) and all of it from Jasmine tests. I don't want to do e2e (for multiple reasons - long story).
@meirka
That's exactly why I built Spectacular, in particular its Feature testing API, preferably used with Angular Testing Library and/or Angular CDK Component Harnesses as described in the docs. Slide deck from NG Poland 2021 coming soon at https://speakerdeck.com/layzee
@LayZeeDK
Thank you, I'll check it out!
I guess I'm not only one who doesn't want to waste time on e2e.
Would wish to see a test for
location.back()
and thus be able to, after back navigation, check if the title and the url is updated correctly.