Skip to content

Commit 656aa69

Browse files
feat: add navigate for router tests (#48)
1 parent 0f4022e commit 656aa69

File tree

7 files changed

+218
-7
lines changed

7 files changed

+218
-7
lines changed

projects/testing-library/src/lib/models.ts

+29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Type } from '@angular/core';
22
import { ComponentFixture } from '@angular/core/testing';
3+
import { Routes } from '@angular/router';
34
import { BoundFunction, FireObject, Queries, queries } from '@testing-library/dom';
45
import { UserEvents } from './user-events';
56

@@ -35,6 +36,12 @@ export interface RenderResult extends RenderResultQueries, FireObject, UserEvent
3536
* For more info see https://angular.io/api/core/testing/ComponentFixture
3637
*/
3738
fixture: ComponentFixture<any>;
39+
/**
40+
* @description
41+
* Navigates to the href of the element or to the path.
42+
*
43+
*/
44+
navigate: (elementOrPath: Element | string, basePath?: string) => Promise<boolean>;
3845
}
3946

4047
export interface RenderOptions<C, Q extends Queries = typeof queries> {
@@ -201,4 +208,26 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
201208
* })
202209
*/
203210
excludeComponentDeclaration?: boolean;
211+
/**
212+
* @description
213+
* The route configuration to set up the router service via `RouterTestingModule.withRoutes`.
214+
* For more info see https://angular.io/api/router/Routes.
215+
*
216+
* @example
217+
* const component = await render(AppComponent, {
218+
* declarations: [ChildComponent],
219+
* routes: [
220+
* {
221+
* path: '',
222+
* children: [
223+
* {
224+
* path: 'child/:id',
225+
* component: ChildComponent
226+
* }
227+
* ]
228+
* }
229+
* ]
230+
* })
231+
*/
232+
routes?: Routes;
204233
}

projects/testing-library/src/lib/testing-library.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Component, DebugElement, ElementRef, OnInit, Type } from '@angular/core';
1+
import { Component, DebugElement, ElementRef, OnInit, Type, NgZone } from '@angular/core';
22
import { ComponentFixture, TestBed } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
5+
import { Router } from '@angular/router';
6+
import { RouterTestingModule } from '@angular/router/testing';
57
import { fireEvent, FireFunction, FireObject, getQueriesForElement, prettyDOM } from '@testing-library/dom';
68
import { RenderOptions, RenderResult } from './models';
79
import { createSelectOptions, createType } from './user-events';
@@ -32,6 +34,7 @@ export async function render<T>(
3234
componentProperties = {},
3335
componentProviders = [],
3436
excludeComponentDeclaration = false,
37+
routes,
3538
} = renderOptions;
3639

3740
const isTemplate = typeof templateOrComponent === 'string';
@@ -44,7 +47,7 @@ export async function render<T>(
4447

4548
TestBed.configureTestingModule({
4649
declarations: [...declarations, ...componentDeclarations],
47-
imports: addAutoImports(imports),
50+
imports: addAutoImports({ imports, routes }),
4851
providers: [...providers],
4952
schemas: [...schemas],
5053
});
@@ -80,6 +83,20 @@ export async function render<T>(
8083
{} as FireFunction & FireObject,
8184
);
8285

86+
let router = routes ? (TestBed.get<Router>(Router) as Router) : null;
87+
const zone = TestBed.get<NgZone>(NgZone) as NgZone;
88+
89+
async function navigate(elementOrPath: Element | string, basePath = '') {
90+
if (!router) {
91+
router = TestBed.get<Router>(Router) as Router;
92+
}
93+
94+
const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href');
95+
96+
await zone.run(() => router.navigate([basePath + href]));
97+
fixture.detectChanges();
98+
}
99+
83100
return {
84101
fixture,
85102
container: fixture.nativeElement,
@@ -89,6 +106,7 @@ export async function render<T>(
89106
...eventsWithDetectChanges,
90107
type: createType(eventsWithDetectChanges),
91108
selectOptions: createSelectOptions(eventsWithDetectChanges),
109+
navigate,
92110
} as any;
93111
}
94112

@@ -168,10 +186,16 @@ function declareComponents({ isTemplate, wrapper, excludeComponentDeclaration, t
168186
return [templateOrComponent];
169187
}
170188

171-
function addAutoImports(imports: any[]) {
172-
if (imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1) {
173-
return imports;
174-
}
189+
function addAutoImports({ imports, routes }: Pick<RenderOptions<any>, 'imports' | 'routes'>) {
190+
const animations = () => {
191+
const animationIsDefined =
192+
imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1;
193+
return animationIsDefined ? [] : [NoopAnimationsModule];
194+
};
195+
196+
const routing = () => {
197+
return routes ? [RouterTestingModule.withRoutes(routes)] : [];
198+
};
175199

176-
return [...imports, NoopAnimationsModule];
200+
return [...imports, ...animations(), ...routing()];
177201
}

src/app/app-routing.module.ts

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MaterialFormsComponent } from './examples/04-forms-with-material';
99
import { ComponentWithProviderComponent } from './examples/05-component-provider';
1010
import { WithNgRxStoreComponent } from './examples/06-with-ngrx-store';
1111
import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
12+
import { MasterComponent, DetailComponent, HiddenDetailComponent } from './examples/09-router';
1213

1314
export const examples = [
1415
{
@@ -67,6 +68,23 @@ export const examples = [
6768
name: 'With NgRx MockStore',
6869
},
6970
},
71+
{
72+
path: 'with-router',
73+
component: MasterComponent,
74+
data: {
75+
name: 'Router',
76+
},
77+
children: [
78+
{
79+
path: 'detail/:id',
80+
component: DetailComponent,
81+
},
82+
{
83+
path: 'hidden-detail',
84+
component: HiddenDetailComponent,
85+
},
86+
],
87+
},
7088
];
7189

7290
export const routes: Routes = [

src/app/app.module.ts

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { MaterialFormsComponent } from './examples/04-forms-with-material';
2020
import { ComponentWithProviderComponent } from './examples/05-component-provider';
2121
import { WithNgRxStoreComponent, reducer } from './examples/06-with-ngrx-store';
2222
import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
23+
import { MasterComponent, DetailComponent, HiddenDetailComponent } from './examples/09-router';
2324

2425
@NgModule({
2526
declarations: [
@@ -34,6 +35,9 @@ import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
3435
ComponentWithProviderComponent,
3536
WithNgRxStoreComponent,
3637
WithNgRxMockStoreComponent,
38+
MasterComponent,
39+
DetailComponent,
40+
HiddenDetailComponent,
3741
],
3842
imports: [
3943
BrowserModule,

src/app/examples/04-forms-with-material.ts

+16
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ import { FormBuilder, Validators, ReactiveFormsModule, ValidationErrors } from '
3434
</div>
3535
</form>
3636
`,
37+
styles: [
38+
`
39+
form {
40+
display: flex;
41+
flex-direction: column;
42+
}
43+
44+
form > * {
45+
width: 100%;
46+
}
47+
48+
[role='alert'] {
49+
color: red;
50+
}
51+
`,
52+
],
3753
})
3854
export class MaterialFormsComponent {
3955
colors = [{ id: 'R', value: 'Red' }, { id: 'B', value: 'Blue' }, { id: 'G', value: 'Green' }];

src/app/examples/09-router.spec.ts

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { render } from '@testing-library/angular';
2+
3+
import { DetailComponent, MasterComponent, HiddenDetailComponent } from './09-router';
4+
import { TestBed } from '@angular/core/testing';
5+
import { Router } from '@angular/router';
6+
7+
test('it can navigate to routes', async () => {
8+
const component = await render(MasterComponent, {
9+
declarations: [DetailComponent, HiddenDetailComponent],
10+
routes: [
11+
{
12+
path: '',
13+
children: [
14+
{
15+
path: 'detail/:id',
16+
component: DetailComponent,
17+
},
18+
{
19+
path: 'hidden-detail',
20+
component: HiddenDetailComponent,
21+
},
22+
],
23+
},
24+
],
25+
});
26+
27+
expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();
28+
29+
await component.navigate(component.getByText(/Load one/));
30+
expect(component.queryByText(/Detail one/i)).toBeInTheDocument();
31+
32+
await component.navigate(component.getByText(/Load three/));
33+
expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();
34+
expect(component.queryByText(/Detail three/i)).toBeInTheDocument();
35+
36+
await component.navigate(component.getByText(/Back to parent/));
37+
expect(component.queryByText(/Detail three/i)).not.toBeInTheDocument();
38+
39+
await component.navigate(component.getByText(/Load two/));
40+
expect(component.queryByText(/Detail two/i)).toBeInTheDocument();
41+
await component.navigate(component.getByText(/hidden x/));
42+
expect(component.queryByText(/You found the treasure!/i)).toBeInTheDocument();
43+
});
44+
45+
test('it can navigate to routes with a base path', async () => {
46+
const basePath = 'base';
47+
const component = await render(MasterComponent, {
48+
declarations: [DetailComponent, HiddenDetailComponent],
49+
routes: [
50+
{
51+
path: basePath,
52+
children: [
53+
{
54+
path: 'detail/:id',
55+
component: DetailComponent,
56+
},
57+
{
58+
path: 'hidden-detail',
59+
component: HiddenDetailComponent,
60+
},
61+
],
62+
},
63+
],
64+
});
65+
66+
expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();
67+
68+
await component.navigate(component.getByText(/Load one/), basePath);
69+
expect(component.queryByText(/Detail one/i)).toBeInTheDocument();
70+
71+
await component.navigate(component.getByText(/Load three/), basePath);
72+
expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();
73+
expect(component.queryByText(/Detail three/i)).toBeInTheDocument();
74+
75+
await component.navigate(component.getByText(/Back to parent/));
76+
expect(component.queryByText(/Detail three/i)).not.toBeInTheDocument();
77+
78+
await component.navigate('base/detail/two'); // possible to just use strings
79+
expect(component.queryByText(/Detail two/i)).toBeInTheDocument();
80+
await component.navigate('/hidden-detail', basePath);
81+
expect(component.queryByText(/You found the treasure!/i)).toBeInTheDocument();
82+
});

src/app/examples/09-router.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { OnInit, Component } from '@angular/core';
2+
import { ActivatedRoute } from '@angular/router';
3+
import { map } from 'rxjs/operators';
4+
5+
@Component({
6+
selector: 'app-master',
7+
template: `
8+
<a [routerLink]="'./detail/one'">Load one</a> | <a [routerLink]="'./detail/two'">Load two</a> |
9+
<a [routerLink]="'./detail/three'">Load three</a> |
10+
11+
<hr />
12+
13+
<router-outlet></router-outlet>
14+
`,
15+
})
16+
export class MasterComponent {}
17+
18+
@Component({
19+
selector: 'app-detail',
20+
template: `
21+
<h2>Detail {{ id | async }}</h2>
22+
23+
<a [routerLink]="'../..'">Back to parent</a>
24+
<a routerLink="/hidden-detail">hidden x</a>
25+
`,
26+
})
27+
export class DetailComponent {
28+
id = this.route.paramMap.pipe(map(params => params.get('id')));
29+
constructor(private route: ActivatedRoute) {}
30+
}
31+
32+
@Component({
33+
selector: 'app-detail-hidden',
34+
template: `
35+
You found the treasure!
36+
`,
37+
})
38+
export class HiddenDetailComponent {}

0 commit comments

Comments
 (0)