Skip to content

Commit

Permalink
feat: add routing support (#151)
Browse files Browse the repository at this point in the history
* feat: initial SpectatorWithRouting proposal

* lint fixes

* docs: update README.md

* refactor: lint fix

* refactor: fix build

* feat: add Jest implementation of SpectatorWithRouting

* fix: lint fixes
  • Loading branch information
dirkluijk committed Aug 26, 2019
1 parent f84e73e commit aacfb25
Show file tree
Hide file tree
Showing 23 changed files with 11,877 additions and 16 deletions.
71 changes: 66 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ import { ButtonComponent } from './button.component';
import { Spectator, createTestComponentFactory } from '@netbasal/spectator';

describe('ButtonComponent', () => {
let spectator: Spectator<ButtonComponent>;
const createComponent = createTestComponentFactory(ButtonComponent);
let spectator: Spectator<ButtonComponent>;

beforeEach(() => spectator = createComponent());

Expand Down Expand Up @@ -80,7 +80,7 @@ const createComponent = createTestComponentFactory({
The `createComponent()` function optionally takes the following options:
```ts
it('should set the title according to the [title] input', () => {
spectator = createComponent({
const spectator = createComponent({
// The component inputs
props: {
title: 'Click'
Expand Down Expand Up @@ -278,6 +278,70 @@ describe('With Custom Host Component', function () {
});
});
```


## Testing Routed Components

For components which use routing, there is a special factory available that extends the default one, and provides a stubbed `ActivatedRoute` so that you can configure additional routing options.

```ts
describe('ButtonComponent', () => {
const createComponent = createRoutedComponentFactory({
component: ProductDetailsComponent,
params: { productId: '3' },
data: { title: 'Some title' }
});

let spectator: SpectatorWithRouting<ProductDetailsComponent>;

beforeEach(() => spectator = createComponent());

it('should display route data title', () => {
expect(spectator.query('.title')).toHaveTest('Some title');
});

it('should react to route changes', () => {
spectator.setParam('productId', '5');

// your test here...
});
});
```

### Updating Route
The `SpectatorWithRoute` API includes convenient methods for updating the current route:

```ts
interface SpectatorWithRouting<C> extends Spectator<C> {
/**
* Simulates a route navigation by updating the Params, QueryParams and Data observable streams.
*/
triggerNavigation(options?: RouteOptions): void;

/**
* Updates the route params and triggers a route navigation.
*/
setRouteParam(name: string, value: string): void;

/**
* Updates the route query params and triggers a route navigation.
*/
setRouteQueryParam(name: string, value: string): void;

/**
* Updates the route data and triggers a route navigation.
*/
setRouteData(name: string, value: string): void;
}
```

### Routing features

* Updating params, queryParams, data and fragments
* Stubs both `ActivatedRoute` and its `ActivatedRouteSnapshot`
* Default Router mocking
* Default RouterLink directive stubs

## Testing Directives
Let's say we have the following directive:

Expand Down Expand Up @@ -472,9 +536,6 @@ We need to create an HTTP factory by using the `createHTTPFactory()` function, p
- `get()` - A proxy for Angular `TestBed.get()`
- `expectOne()` - Expect that a single request was made which matches the given URL and it's method, and return its mock request

## Routing Testing
TODO

## Global Injections
It's possible to define injections which will be available for each test without the need to re-declare them in each test:
```ts
Expand Down
2 changes: 1 addition & 1 deletion projects/spectator/jest/src/lib/spectator-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface SpectatorService<S> extends BaseSpectatorService<S> {
}

/**
* @pubicApi
* @publicApi
*/
export type SpectatorServiceFactory<S> = (overrides?: SpectatorServiceOverrides<S>) => SpectatorService<S>;

Expand Down
14 changes: 12 additions & 2 deletions projects/spectator/jest/src/lib/spectator-with-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,34 @@ import {
isType,
HostComponent,
SpectatorWithHost as BaseSpectatorWithHost,
SpectatorWithHostFactory,
SpectatorWithHostOptions,
SpectatorWithHostOverrides,
Token
} from '@netbasal/spectator';

import { mockProvider, SpyObject } from './mock';

/**
* @publicApi
*/
export class SpectatorWithHost<C, H = HostComponent> extends BaseSpectatorWithHost<C, H> {
public get<T>(type: Token<T> | Token<any>, fromComponentInjector: boolean = false): SpyObject<T> {
return super.get(type, fromComponentInjector) as SpyObject<T>;
}
}

/**
* @publicApi
*/
export type SpectatorWithHostFactory<C, H> = (template: string, overrides?: SpectatorWithHostOverrides<C, H>) => SpectatorWithHost<C, H>;
/**
* @publicApi
*/
export function createHostComponentFactory<C, H = HostComponent>(
typeOrOptions: SpectatorWithHostOptions<C, H> | Type<C>
): SpectatorWithHostFactory<C, H> {
return baseCreateHostComponentFactory({
mockProvider,
...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions)
});
}) as SpectatorWithHostFactory<C, H>;
}
35 changes: 35 additions & 0 deletions projects/spectator/jest/src/lib/spectator-with-routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Type } from '@angular/core';
import {
createRoutedComponentFactory as baseCreateRoutedComponentFactory,
isType,
SpectatorWithRouting as BaseSpectatorWithRouting,
SpectatorWithRoutingOptions,
SpectatorWithRoutingOverrides,
Token
} from '@netbasal/spectator';

import { mockProvider, SpyObject } from './mock';

/**
* @publicApi
*/
export class SpectatorWithRouting<C> extends BaseSpectatorWithRouting<C> {
public get<T>(type: Token<T> | Token<any>, fromComponentInjector: boolean = false): SpyObject<T> {
return super.get(type, fromComponentInjector) as SpyObject<T>;
}
}

/**
* @publicApi
*/
export type SpectatorWithRoutingFactory<C> = (overrides?: SpectatorWithRoutingOverrides<C>) => SpectatorWithRouting<C>;

/**
* @publicApi
*/
export function createRoutedComponentFactory<C>(typeOrOptions: SpectatorWithRoutingOptions<C> | Type<C>): SpectatorWithRoutingFactory<C> {
return baseCreateRoutedComponentFactory({
mockProvider,
...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions)
}) as SpectatorWithRoutingFactory<C>;
}
1 change: 1 addition & 0 deletions projects/spectator/jest/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './lib/spectator';
export * from './lib/spectator-http';
export * from './lib/spectator-service';
export * from './lib/spectator-with-host';
export * from './lib/spectator-with-routing';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('ComponentWithoutOverwrittenProvidersComponent', () => {
componentProviders: [mockProvider(DummyService)]
});

it("should not overwrite component's providers and work using createHostComponentFactory", () => {
it('should not overwrite component\'s providers and work using createHostComponentFactory', () => {
const { component } = createHost(`
<app-component-without-overwritten-providers>
</app-component-without-overwritten-providers>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ describe('ViewChildrenComponent', () => {
const serviceFromChild = spectator.query(ChildComponent, { read: ChildServiceService });
const div = spectator.query('div');
const component = spectator.query(ChildComponent);
const { nativeElement } = spectator.query(ChildComponent, {
spectator.query(ChildComponent, {
read: ElementRef
})!;
});
const button = spectator.query('button');

expect(serviceFromChild).toBeDefined();
Expand Down
112 changes: 112 additions & 0 deletions projects/spectator/jest/test/with-routing/my-page.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Router, RouterLink } from '@angular/router';
import { createRoutedComponentFactory } from '@netbasal/spectator/jest';

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

describe('MyPageComponent', () => {
describe('simple use', () => {
const createComponent = createRoutedComponentFactory(MyPageComponent);

it('should create', () => {
const spectator = createComponent();

expect(spectator.query('.foo')).toExist();
});
});

describe('route options', () => {
const createComponent = createRoutedComponentFactory({
component: MyPageComponent,
data: { title: 'lorem', dynamicTitle: 'ipsum' },
params: { foo: '1', bar: '2' },
queryParams: { baz: '3' }
});

it('should create with default options', () => {
const spectator = createComponent();

expect(spectator.query('.title')).toHaveText('lorem');
expect(spectator.query('.dynamic-title')).toHaveText('ipsum');

expect(spectator.query('.foo')).toHaveText('1');
expect(spectator.query('.bar')).toHaveText('2');
expect(spectator.query('.baz')).toHaveText('3');
});

it('should create with overridden options', () => {
const spectator = createComponent({
params: { foo: 'A', bar: 'B' }
});

expect(spectator.query('.foo')).toHaveText('A');
expect(spectator.query('.bar')).toHaveText('B');
expect(spectator.query('.baz')).toHaveText('3');
});

it('should respond to updates', () => {
const spectator = createComponent({
params: { foo: 'A', bar: 'B' }
});

expect(spectator.query('.foo')).toHaveText('A');
expect(spectator.query('.bar')).toHaveText('B');
expect(spectator.query('.baz')).toHaveText('3');

spectator.setRouteParam('bar', 'X');

expect(spectator.query('.foo')).toHaveText('A');
expect(spectator.query('.bar')).toHaveText('X');
expect(spectator.query('.baz')).toHaveText('3');
expect(spectator.component.fragment).toBeNull();

spectator.setRouteQueryParam('baz', 'Y');
spectator.setRouteFragment('lorem');

expect(spectator.query('.foo')).toHaveText('A');
expect(spectator.query('.bar')).toHaveText('X');
expect(spectator.query('.baz')).toHaveText('Y');
expect(spectator.component.fragment).toBe('lorem');
});

it('should support snapshot data', () => {
const spectator = createComponent();

expect(spectator.query('.title')).toHaveText('lorem');
expect(spectator.query('.dynamic-title')).toHaveText('ipsum');

spectator.triggerNavigation({
data: { title: 'new-title', dynamicTitle: 'new-dynamic-title' }
});

expect(spectator.query('.title')).toHaveText('lorem');
expect(spectator.query('.dynamic-title')).toHaveText('new-dynamic-title');
});
});

describe('routerLinks', () => {
const createComponent = createRoutedComponentFactory(MyPageComponent);

it('should mock routerLinks', () => {
const spectator = createComponent();

// tslint:disable-next-line:no-unnecessary-type-assertion
const link1 = spectator.query('.link-1', { read: RouterLink })!;

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

describe('default router mocking', () => {
const createComponent = createRoutedComponentFactory({
component: MyPageComponent
});

it('should support mocks', () => {
const spectator = createComponent();

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

expect(spectator.get(Router).navigate).toHaveBeenCalledWith(['bar']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { SpectatorWithHost } from './spectator-with-host';
/**
* @publicApi
*/
export type SpectatorWithHostFactory<C, H> = (template: string, options?: SpectatorWithHostOverrides<C, H>) => SpectatorWithHost<C, H>;
export type SpectatorWithHostFactory<C, H> = (template: string, overrides?: SpectatorWithHostOverrides<C, H>) => SpectatorWithHost<C, H>;

/**
* @publicApi
Expand Down
Loading

0 comments on commit aacfb25

Please sign in to comment.