Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(routing): add option to disable stubs and use RouterTestingModule #188

Merged
merged 2 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ describe('ButtonComponent', () => {
});
```

### Updating Route
### Triggering a navigation
The `SpectatorRouting` API includes convenient methods for updating the current route:

```ts
Expand Down Expand Up @@ -379,11 +379,62 @@ interface SpectatorRouting<C> extends Spectator<C> {
}
```

### Routing Features
### Integration testing with `RouterTestingModule`

* It automatically provides a stub implementation for `ActivatedRoute`
* You can configure the `params`, `queryParams`, `fragments` and `data`. You can also update them, to test how your component reacts on changes.
* It provides a stub for `RouterLink` directives
If you set the `stubsEnabled` option to `false`, you can pass a real routing configuration
and setup an integration test using the `RouterTestingModule` from Angular.

Note that this requires promises to resolve. One way to deal with this, is by making your test async:

```ts
describe('Routing integration test', () => {
const createComponent = createRoutingFactory({
component: MyComponent,
declarations: [OtherComponent],
stubsEnabled: false,
routes: [
{
path: '',
component: MyComponent
},
{
path: 'foo',
component: OtherComponent
}
]
});

it('should navigate away using router link', async () => {
const spectator = createComponent();

// wait for promises to resolve...
await spectator.fixture.whenStable();

// test the current route by asserting the location
expect(spectator.get(Location).path()).toBe('/');

// click on a router link
spectator.click('.link-1');

// don't forget to wait for promises to resolve...
await spectator.fixture.whenStable();

// test the new route by asserting the location
expect(spectator.get(Location).path()).toBe('/foo');
});
});
```

### Routing Options

The `createRoutesFactory` function can take the following options, on top of the default Spectator options:

* `params`: initial params to use in `ActivatedRoute` stub
* `queryParams`: initial query params to use in `ActivatedRoute` stub
* `data`: initial data to use in `ActivatedRoute` stub
* `fragment`: initial fragment to use in `ActivatedRoute` stub
* `stubsEnabled` (default: `true`): enables the `ActivatedRoute` stub, if set to `false` it uses `RouterTestingModule` instead
* `routes`: if `stubsEnabled` is set to false, you can pass a `Routes` configuration for `RouterTestingModule`

## Testing Directives

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('MyPageComponent', () => {
// tslint:disable-next-line:no-unnecessary-type-assertion
const link1 = spectator.query('.link-1', { read: RouterLink })!;

expect(link1.routerLink).toEqual(['foo']);
expect(link1.routerLink).toEqual(['/foo']);
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Provider, Type } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';

import { setProps } from '../internals/query';
import * as customMatchers from '../matchers';
Expand Down Expand Up @@ -71,6 +71,8 @@ export function createRoutingFactory<C>(typeOrOptions: Type<C> | SpectatorRoutin

const spectator = createSpectatorRouting(options, props);

spectator.router.initialNavigation();

if (options.detectChanges && detectChanges) {
spectator.detectChanges();
}
Expand All @@ -85,5 +87,5 @@ function createSpectatorRouting<C>(options: Required<SpectatorRoutingOptions<C>>

const component = setProps(fixture.componentInstance, props);

return new SpectatorRouting(fixture, debugElement, component, TestBed.get(ActivatedRoute));
return new SpectatorRouting(fixture, debugElement, component, TestBed.get(Router), TestBed.get(ActivatedRoute));
}
39 changes: 30 additions & 9 deletions projects/spectator/src/lib/spectator-routing/initial-module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { EMPTY } from 'rxjs';

import { ModuleMetadata } from '../base/initial-module';
import { initialSpectatorModule } from '../spectator/initial-module';
import { SpyObject } from '../mock';

import { ActivatedRouteStub } from './activated-route-stub';
import { SpectatorRoutingOptions } from './options';
Expand All @@ -13,23 +16,41 @@ import { RouterLinkDirectiveStub } from './router-link-stub';
export function initialRoutingModule<S>(options: Required<SpectatorRoutingOptions<S>>): ModuleMetadata {
const moduleMetadata = initialSpectatorModule(options);

if (options.mockRouterLinks) {
if (options.mockRouterLinks && options.stubsEnabled) {
moduleMetadata.declarations.push(RouterLinkDirectiveStub);
}

moduleMetadata.providers.push(options.mockProvider(Router));
if (options.stubsEnabled) {
moduleMetadata.providers.push({
provide: Router,
useFactory: () => {
const provider = options.mockProvider(Router);
const router = provider.useFactory() as SpyObject<Router>;

moduleMetadata.providers.push([
{
provide: ActivatedRoute,
useFactory: () =>
new ActivatedRouteStub({
// this prevents the events property to be undefined
router.castToWritable().events = EMPTY;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if someone needs to test the router events? They might register a subscription in the component and want to test it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, for now I assume that you should provide your own mock provider if you want to configure mocks more advanced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just do the following?

options.mockProvider(Router, {
  events = new Subject();
  emitRouterEvent(e) {
    this.events.next(e)
  }
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all right, will do

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, can you check again?


return router;
}
});

moduleMetadata.providers.push(
{
provide: ActivatedRouteStub,
useValue: new ActivatedRouteStub({
params: options.params,
queryParams: options.queryParams,
data: options.data
})
}
]);
},
{
provide: ActivatedRoute,
useExisting: ActivatedRouteStub
}
);
} else {
moduleMetadata.imports.push(RouterTestingModule.withRoutes(options.routes));
}

return moduleMetadata;
}
8 changes: 7 additions & 1 deletion projects/spectator/src/lib/spectator-routing/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Routes } from '@angular/router';

import { merge } from '../internals/merge';
import { getSpectatorDefaultOptions, SpectatorOptions } from '../spectator/options';
import { OptionalsRequired } from '../types';
Expand All @@ -7,6 +9,8 @@ import { RouteOptions } from './route-options';
export type SpectatorRoutingOptions<C> = SpectatorOptions<C> &
RouteOptions & {
mockRouterLinks?: boolean;
stubsEnabled?: boolean;
routes?: Routes;
};

const defaultRoutingOptions: OptionalsRequired<SpectatorRoutingOptions<any>> = {
Expand All @@ -15,7 +19,9 @@ const defaultRoutingOptions: OptionalsRequired<SpectatorRoutingOptions<any>> = {
queryParams: {},
data: {},
fragment: null,
mockRouterLinks: true
mockRouterLinks: true,
stubsEnabled: true,
routes: []
};

/**
Expand Down
50 changes: 39 additions & 11 deletions projects/spectator/src/lib/spectator-routing/spectator-routing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DebugElement, Type } from '@angular/core';
import { DebugElement } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
import { Router } from '@angular/router';

import { Spectator } from '../spectator/spectator';

Expand All @@ -14,7 +15,8 @@ export class SpectatorRouting<C> extends Spectator<C> {
fixture: ComponentFixture<any>,
debugElement: DebugElement,
instance: C,
private readonly activatedRouteStub: ActivatedRouteStub
public readonly router: Router,
public readonly activatedRouteStub?: ActivatedRouteStub
) {
super(fixture, debugElement, instance, debugElement.nativeElement);
}
Expand All @@ -23,6 +25,10 @@ export class SpectatorRouting<C> extends Spectator<C> {
* Simulates a route navigation by updating the Params, QueryParams and Data observable streams.
*/
public triggerNavigation(options?: RouteOptions): void {
if (!this.checkStubPresent()) {
return;
}

if (options && options.params) {
this.activatedRouteStub.setParams(options.params);
}
Expand All @@ -46,36 +52,58 @@ export class SpectatorRouting<C> extends Spectator<C> {
* Updates the route params and triggers a route navigation.
*/
public setRouteParam(name: string, value: string): void {
this.activatedRouteStub.setParam(name, value);
this.triggerNavigationAndUpdate();
if (this.checkStubPresent()) {
this.activatedRouteStub.setParam(name, value);
this.triggerNavigationAndUpdate();
}
}

/**
* Updates the route query params and triggers a route navigation.
*/
public setRouteQueryParam(name: string, value: string): void {
this.activatedRouteStub.setQueryParam(name, value);
this.triggerNavigationAndUpdate();
if (this.checkStubPresent()) {
this.activatedRouteStub.setQueryParam(name, value);
this.triggerNavigationAndUpdate();
}
}

/**
* Updates the route data and triggers a route navigation.
*/
public setRouteData(name: string, value: string): void {
this.activatedRouteStub.setData(name, value);
this.triggerNavigationAndUpdate();
if (this.checkStubPresent()) {
this.activatedRouteStub.setData(name, value);
this.triggerNavigationAndUpdate();
}
}

/**
* Updates the route fragment and triggers a route navigation.
*/
public setRouteFragment(fragment: string | null): void {
this.activatedRouteStub.setFragment(fragment);
this.triggerNavigationAndUpdate();
if (this.checkStubPresent()) {
this.activatedRouteStub.setFragment(fragment);
this.triggerNavigationAndUpdate();
}
}

private triggerNavigationAndUpdate(): void {
this.activatedRouteStub.triggerNavigation();
this.activatedRouteStub!.triggerNavigation();
this.detectChanges();
}

private checkStubPresent(): this is { readonly activatedRouteStub: ActivatedRouteStub } {
if (!this.activatedRouteStub) {
// tslint:disable-next-line:no-console
console.warn(
'No stub for ActivatedRoute present. Set Spectator option "stubsEnabled" to true if you want to use this ' +
'helper, or use Router to trigger navigation.'
);

return false;
}

return true;
}
}
53 changes: 52 additions & 1 deletion projects/spectator/test/with-routing/my-page.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Router, RouterLink } from '@angular/router';
import { createRoutingFactory } from '@ngneat/spectator';
import { Component } from '@angular/core';
import { Location } from '@angular/common';

import { MyPageComponent } from './my-page.component';

Expand Down Expand Up @@ -91,7 +93,7 @@ describe('MyPageComponent', () => {

const link1 = spectator.query('.link-1', { read: RouterLink })!;

expect(link1.routerLink).toEqual(['foo']);
expect(link1.routerLink).toEqual(['/foo']);
});
});

Expand All @@ -108,4 +110,53 @@ describe('MyPageComponent', () => {
expect(spectator.get(Router).navigate).toHaveBeenCalledWith(['bar']);
});
});

describe('without stubs', () => {
@Component({
selector: 'dummy',
template: ''
})
class DummyComponent {}

const createComponent = createRoutingFactory({
component: MyPageComponent,
declarations: [DummyComponent],
stubsEnabled: false,
routes: [
{
path: '',
component: MyPageComponent
},
{
path: 'foo',
component: DummyComponent
}
]
});

it('should navigate away using router', async () => {
const spectator = createComponent();

await spectator.fixture.whenStable();
expect(spectator.get(Location).path()).toBe('/');

await spectator.router.navigate(['/foo']);
expect(spectator.get(Location).path()).toBe('/foo');

await spectator.router.navigate(['/']);
expect(spectator.get(Location).path()).toBe('/');
});

it('should navigate away using router link', async () => {
const spectator = createComponent();

await spectator.fixture.whenStable();
expect(spectator.get(Location).path()).toBe('/');

spectator.click('.link-1');

await spectator.fixture.whenStable();
expect(spectator.get(Location).path()).toBe('/foo');
});
});
});
2 changes: 1 addition & 1 deletion projects/spectator/test/with-routing/my-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { map } from 'rxjs/operators';
<div class="foo">{{ foo }}</div>
<div class="bar">{{ bar }}</div>
<div class="baz">{{ baz$ | async }}</div>
<a class="link-1" [routerLink]="['foo']">Some link</a>
<a class="link-1" [routerLink]="['/foo']">Some link</a>
<a class="link-2" (click)="navigate()">Other link</a>
`
})
Expand Down