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 all commits
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
8 changes: 4 additions & 4 deletions projects/spectator/jest/src/lib/mock.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { FactoryProvider, Type } from '@angular/core';
import { installProtoMethods, CompatibleSpy, SpyObject as BaseSpyObject } from '@ngneat/spectator';
import { FactoryProvider } from '@angular/core';
import { installProtoMethods, CompatibleSpy, SpyObject as BaseSpyObject, InjectableType } from '@ngneat/spectator';

export type SpyObject<T> = BaseSpyObject<T> & { [P in keyof T]: T[P] & (T[P] extends (...args: any[]) => infer R ? jest.Mock<R> : T[P]) };

/**
* @internal
*/
export function createSpyObject<T>(type: Type<T>, template?: Partial<Record<keyof T, any>>): SpyObject<T> {
export function createSpyObject<T>(type: InjectableType<T>, template?: Partial<Record<keyof T, any>>): SpyObject<T> {
const mock: any = template || {};

installProtoMethods(mock, type.prototype, () => {
Expand Down Expand Up @@ -36,7 +36,7 @@ export function createSpyObject<T>(type: Type<T>, template?: Partial<Record<keyo
/**
* @publicApi
*/
export function mockProvider<T>(type: Type<T>, properties?: Partial<Record<keyof T, any>>): FactoryProvider {
export function mockProvider<T>(type: InjectableType<T>, properties?: Partial<Record<keyof T, any>>): FactoryProvider {
return {
provide: type,
useFactory: () => createSpyObject(type, properties)
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
6 changes: 4 additions & 2 deletions projects/spectator/src/lib/mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** Credit: Valentin Buryakov */
import { FactoryProvider, Type } from '@angular/core';

import { InjectableType } from './token';

type Writable<T> = { -readonly [P in keyof T]: T[P] };

/**
Expand Down Expand Up @@ -69,7 +71,7 @@ export function installProtoMethods<T>(mock: any, proto: any, createSpyFn: Funct
/**
* @publicApi
*/
export function createSpyObject<T>(type: Type<T>, template?: Partial<Record<keyof T, any>>): SpyObject<T> {
export function createSpyObject<T>(type: InjectableType<T>, template?: Partial<Record<keyof T, any>>): SpyObject<T> {
const mock: any = { ...template } || {};

installProtoMethods<T>(mock, type.prototype, name => {
Expand All @@ -89,7 +91,7 @@ export function createSpyObject<T>(type: Type<T>, template?: Partial<Record<keyo
/**
* @publicApi
*/
export function mockProvider<T>(type: Type<T>, properties?: Partial<Record<keyof T, any>>): FactoryProvider {
export function mockProvider<T>(type: InjectableType<T>, properties?: Partial<Record<keyof T, any>>): FactoryProvider {
return {
provide: type,
useFactory: () => createSpyObject(type, properties)
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));
}
41 changes: 31 additions & 10 deletions projects/spectator/src/lib/spectator-routing/initial-module.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,56 @@
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Event, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Subject } from 'rxjs';

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

import { ActivatedRouteStub } from './activated-route-stub';
import { SpectatorRoutingOptions } from './options';
import { RouterLinkDirectiveStub } from './router-link-stub';
import { RouterStub } from './router-stub';

/**
* @internal
*/
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(
options.mockProvider(RouterStub, {
events: new Subject<Event>(),
emitRouterEvent(event: Event): void {
this.events.next(event);
}
}),
{
provide: Router,
useExisting: RouterStub
}
);

moduleMetadata.providers.push([
{
provide: ActivatedRoute,
useFactory: () =>
new ActivatedRouteStub({
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
9 changes: 9 additions & 0 deletions projects/spectator/src/lib/spectator-routing/router-stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router, Event } from '@angular/router';

export abstract class RouterStub extends Router {
public abstract emitRouterEvent(event: Event): void;
}

export function isRouterStub(router: Router): router is RouterStub {
return 'emitRouterEvent' in router;
}
68 changes: 57 additions & 11 deletions projects/spectator/src/lib/spectator-routing/spectator-routing.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { DebugElement, Type } from '@angular/core';
import { DebugElement } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
import { Event, Router } from '@angular/router';

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

import { ActivatedRouteStub } from './activated-route-stub';
import { RouteOptions } from './route-options';
import { isRouterStub } from './router-stub';

/**
* @publicApi
Expand All @@ -14,7 +16,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 +26,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 +53,75 @@ 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();
}
}

/**
* Emits a router event
*/
public emitRouterEvent(event: Event): void {
if (!isRouterStub(this.router)) {
// tslint:disable-next-line:no-console
console.warn(
'No stub for Router present. Set Spectator option "stubsEnabled" to true if you want to use this ' +
'helper, or use Router navigation to trigger events.'
);

return;
}

this.router.emitRouterEvent(event);
}

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;
}
}
Loading