Skip to content

Commit

Permalink
feat: add navigate for router tests (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
timdeschryver authored Sep 13, 2019
1 parent 0f4022e commit 656aa69
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 7 deletions.
29 changes: 29 additions & 0 deletions projects/testing-library/src/lib/models.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Type } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
import { Routes } from '@angular/router';
import { BoundFunction, FireObject, Queries, queries } from '@testing-library/dom';
import { UserEvents } from './user-events';

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

export interface RenderOptions<C, Q extends Queries = typeof queries> {
Expand Down Expand Up @@ -201,4 +208,26 @@ export interface RenderOptions<C, Q extends Queries = typeof queries> {
* })
*/
excludeComponentDeclaration?: boolean;
/**
* @description
* The route configuration to set up the router service via `RouterTestingModule.withRoutes`.
* For more info see https://angular.io/api/router/Routes.
*
* @example
* const component = await render(AppComponent, {
* declarations: [ChildComponent],
* routes: [
* {
* path: '',
* children: [
* {
* path: 'child/:id',
* component: ChildComponent
* }
* ]
* }
* ]
* })
*/
routes?: Routes;
}
38 changes: 31 additions & 7 deletions projects/testing-library/src/lib/testing-library.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Component, DebugElement, ElementRef, OnInit, Type } from '@angular/core';
import { Component, DebugElement, ElementRef, OnInit, Type, NgZone } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { fireEvent, FireFunction, FireObject, getQueriesForElement, prettyDOM } from '@testing-library/dom';
import { RenderOptions, RenderResult } from './models';
import { createSelectOptions, createType } from './user-events';
Expand Down Expand Up @@ -32,6 +34,7 @@ export async function render<T>(
componentProperties = {},
componentProviders = [],
excludeComponentDeclaration = false,
routes,
} = renderOptions;

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

TestBed.configureTestingModule({
declarations: [...declarations, ...componentDeclarations],
imports: addAutoImports(imports),
imports: addAutoImports({ imports, routes }),
providers: [...providers],
schemas: [...schemas],
});
Expand Down Expand Up @@ -80,6 +83,20 @@ export async function render<T>(
{} as FireFunction & FireObject,
);

let router = routes ? (TestBed.get<Router>(Router) as Router) : null;
const zone = TestBed.get<NgZone>(NgZone) as NgZone;

async function navigate(elementOrPath: Element | string, basePath = '') {
if (!router) {
router = TestBed.get<Router>(Router) as Router;
}

const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href');

await zone.run(() => router.navigate([basePath + href]));
fixture.detectChanges();
}

return {
fixture,
container: fixture.nativeElement,
Expand All @@ -89,6 +106,7 @@ export async function render<T>(
...eventsWithDetectChanges,
type: createType(eventsWithDetectChanges),
selectOptions: createSelectOptions(eventsWithDetectChanges),
navigate,
} as any;
}

Expand Down Expand Up @@ -168,10 +186,16 @@ function declareComponents({ isTemplate, wrapper, excludeComponentDeclaration, t
return [templateOrComponent];
}

function addAutoImports(imports: any[]) {
if (imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1) {
return imports;
}
function addAutoImports({ imports, routes }: Pick<RenderOptions<any>, 'imports' | 'routes'>) {
const animations = () => {
const animationIsDefined =
imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1;
return animationIsDefined ? [] : [NoopAnimationsModule];
};

const routing = () => {
return routes ? [RouterTestingModule.withRoutes(routes)] : [];
};

return [...imports, NoopAnimationsModule];
return [...imports, ...animations(), ...routing()];
}
18 changes: 18 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MaterialFormsComponent } from './examples/04-forms-with-material';
import { ComponentWithProviderComponent } from './examples/05-component-provider';
import { WithNgRxStoreComponent } from './examples/06-with-ngrx-store';
import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
import { MasterComponent, DetailComponent, HiddenDetailComponent } from './examples/09-router';

export const examples = [
{
Expand Down Expand Up @@ -67,6 +68,23 @@ export const examples = [
name: 'With NgRx MockStore',
},
},
{
path: 'with-router',
component: MasterComponent,
data: {
name: 'Router',
},
children: [
{
path: 'detail/:id',
component: DetailComponent,
},
{
path: 'hidden-detail',
component: HiddenDetailComponent,
},
],
},
];

export const routes: Routes = [
Expand Down
4 changes: 4 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MaterialFormsComponent } from './examples/04-forms-with-material';
import { ComponentWithProviderComponent } from './examples/05-component-provider';
import { WithNgRxStoreComponent, reducer } from './examples/06-with-ngrx-store';
import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
import { MasterComponent, DetailComponent, HiddenDetailComponent } from './examples/09-router';

@NgModule({
declarations: [
Expand All @@ -34,6 +35,9 @@ import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
ComponentWithProviderComponent,
WithNgRxStoreComponent,
WithNgRxMockStoreComponent,
MasterComponent,
DetailComponent,
HiddenDetailComponent,
],
imports: [
BrowserModule,
Expand Down
16 changes: 16 additions & 0 deletions src/app/examples/04-forms-with-material.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ import { FormBuilder, Validators, ReactiveFormsModule, ValidationErrors } from '
</div>
</form>
`,
styles: [
`
form {
display: flex;
flex-direction: column;
}
form > * {
width: 100%;
}
[role='alert'] {
color: red;
}
`,
],
})
export class MaterialFormsComponent {
colors = [{ id: 'R', value: 'Red' }, { id: 'B', value: 'Blue' }, { id: 'G', value: 'Green' }];
Expand Down
82 changes: 82 additions & 0 deletions src/app/examples/09-router.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { render } from '@testing-library/angular';

import { DetailComponent, MasterComponent, HiddenDetailComponent } from './09-router';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';

test('it can navigate to routes', async () => {
const component = await render(MasterComponent, {
declarations: [DetailComponent, HiddenDetailComponent],
routes: [
{
path: '',
children: [
{
path: 'detail/:id',
component: DetailComponent,
},
{
path: 'hidden-detail',
component: HiddenDetailComponent,
},
],
},
],
});

expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();

await component.navigate(component.getByText(/Load one/));
expect(component.queryByText(/Detail one/i)).toBeInTheDocument();

await component.navigate(component.getByText(/Load three/));
expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();
expect(component.queryByText(/Detail three/i)).toBeInTheDocument();

await component.navigate(component.getByText(/Back to parent/));
expect(component.queryByText(/Detail three/i)).not.toBeInTheDocument();

await component.navigate(component.getByText(/Load two/));
expect(component.queryByText(/Detail two/i)).toBeInTheDocument();
await component.navigate(component.getByText(/hidden x/));
expect(component.queryByText(/You found the treasure!/i)).toBeInTheDocument();
});

test('it can navigate to routes with a base path', async () => {
const basePath = 'base';
const component = await render(MasterComponent, {
declarations: [DetailComponent, HiddenDetailComponent],
routes: [
{
path: basePath,
children: [
{
path: 'detail/:id',
component: DetailComponent,
},
{
path: 'hidden-detail',
component: HiddenDetailComponent,
},
],
},
],
});

expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();

await component.navigate(component.getByText(/Load one/), basePath);
expect(component.queryByText(/Detail one/i)).toBeInTheDocument();

await component.navigate(component.getByText(/Load three/), basePath);
expect(component.queryByText(/Detail one/i)).not.toBeInTheDocument();
expect(component.queryByText(/Detail three/i)).toBeInTheDocument();

await component.navigate(component.getByText(/Back to parent/));
expect(component.queryByText(/Detail three/i)).not.toBeInTheDocument();

await component.navigate('base/detail/two'); // possible to just use strings
expect(component.queryByText(/Detail two/i)).toBeInTheDocument();
await component.navigate('/hidden-detail', basePath);
expect(component.queryByText(/You found the treasure!/i)).toBeInTheDocument();
});
38 changes: 38 additions & 0 deletions src/app/examples/09-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { OnInit, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';

@Component({
selector: 'app-master',
template: `
<a [routerLink]="'./detail/one'">Load one</a> | <a [routerLink]="'./detail/two'">Load two</a> |
<a [routerLink]="'./detail/three'">Load three</a> |
<hr />
<router-outlet></router-outlet>
`,
})
export class MasterComponent {}

@Component({
selector: 'app-detail',
template: `
<h2>Detail {{ id | async }}</h2>
<a [routerLink]="'../..'">Back to parent</a>
<a routerLink="/hidden-detail">hidden x</a>
`,
})
export class DetailComponent {
id = this.route.paramMap.pipe(map(params => params.get('id')));
constructor(private route: ActivatedRoute) {}
}

@Component({
selector: 'app-detail-hidden',
template: `
You found the treasure!
`,
})
export class HiddenDetailComponent {}

0 comments on commit 656aa69

Please sign in to comment.