Skip to content

Commit

Permalink
fix(spectator): improve type inference even more
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkluijk committed Aug 2, 2019
1 parent 250073a commit 877013b
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 26 deletions.
6 changes: 3 additions & 3 deletions projects/spectator/jest/src/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
* found in the LICENSE file at https://github.com/NetanelBasal/spectator/blob/master/LICENSE
*/

import { InjectionToken, Type } from '@angular/core';
import { SpectatorWithHost as BaseSpectatorWithHost, createHostComponentFactory as baseCreateHostComponentFactory, HostComponent, SpectatorOptions, isType } from '@netbasal/spectator';
import { Type } from '@angular/core';
import { SpectatorWithHost as BaseSpectatorWithHost, createHostComponentFactory as baseCreateHostComponentFactory, HostComponent, SpectatorOptions, isType, Token } from '@netbasal/spectator';

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

export class SpectatorWithHost<C, H = HostComponent> extends BaseSpectatorWithHost<C, H> {
/**
* @inheritDoc
*/
get<T>(type: Type<T> | InjectionToken<T>, fromComponentInjector = false): T & SpyObject<T> {
get<T>(type: Token<T>, fromComponentInjector = false): T & SpyObject<T> {
return super.get(type, fromComponentInjector) as T & SpyObject<T>;
}
}
Expand Down
5 changes: 2 additions & 3 deletions projects/spectator/jest/src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
* found in the LICENSE file at https://github.com/NetanelBasal/spectator/blob/master/LICENSE
*/

import { InjectionToken, Type } from '@angular/core';
import { Spectator as BaseSpectator } from '@netbasal/spectator';
import { Spectator as BaseSpectator, Token } from '@netbasal/spectator';

import { SpyObject } from './mock';

export class Spectator<C> extends BaseSpectator<C> {
/**
* @inheritDoc
*/
get<T>(type: Type<T> | InjectionToken<T>, fromComponentInjector = false): T & SpyObject<T> {
get<T>(type: Token<T> | Token<any>, fromComponentInjector = false): T & SpyObject<T> {
return super.get(type, fromComponentInjector) as T & SpyObject<T>;
}
}
6 changes: 3 additions & 3 deletions projects/spectator/jest/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
* found in the LICENSE file at https://github.com/NetanelBasal/spectator/blob/master/LICENSE
*/

import { InjectionToken, Type } from '@angular/core';
import { SpectatorService as BaseSpectatorService, createService as baseCreateService, isType, ServiceParams } from '@netbasal/spectator';
import { Type } from '@angular/core';
import { SpectatorService as BaseSpectatorService, createService as baseCreateService, isType, ServiceParams, Token } from '@netbasal/spectator';

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

export interface SpectatorService<S> extends BaseSpectatorService<S> {
get<T>(token: Type<T> | InjectionToken<T>): T & SpyObject<T>;
get<T>(token: Token<T> | Token<any>): T & SpyObject<T>;
}

export function createService<S>(options: ServiceParams<S> | Type<S>): SpectatorService<S> {
Expand Down
8 changes: 6 additions & 2 deletions projects/spectator/src/lib/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import { DebugElement, Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Token } from '@netbasal/spectator';

import { _getChild, _getChildren, _setInput, Spectator } from './internals';
import * as customMatchers from './matchers';
import { By } from '@angular/platform-browser';
Expand Down Expand Up @@ -44,7 +46,8 @@ export class SpectatorWithHost<C, H = HostComponent> extends Spectator<C> {
* @returns
*/
queryHost<T extends Element>(directiveOrSelector: string): T;
queryHost<T>(directiveOrSelector: Type<T>, options?: { read }): T;
queryHost<T>(directiveOrSelector: Type<T>): T;
queryHost<T>(directiveOrSelector: Type<any>, options: { read: Token<T> }): T;
queryHost<T>(directiveOrSelector: Type<T> | string, options: { read } = { read: undefined }): T {
return _getChild<T>(this.hostDebugElement)(directiveOrSelector, options);
}
Expand All @@ -56,7 +59,8 @@ export class SpectatorWithHost<C, H = HostComponent> extends Spectator<C> {
* @returns
*/
queryHostAll<T extends Element>(directiveOrSelector: string): T[];
queryHostAll<T>(directiveOrSelector: Type<T>, options?: { read }): T[];
queryHostAll<T>(directiveOrSelector: Type<T>): T[];
queryHostAll<T>(directiveOrSelector: Type<any>, options: { read: Token<T> }): T[];
queryHostAll<T>(directiveOrSelector: Type<T> | string, options: { read } = { read: undefined }): T[] {
return _getChildren<T>(this.hostDebugElement)(directiveOrSelector, options);
}
Expand Down
6 changes: 4 additions & 2 deletions projects/spectator/src/lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import { async, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';
import { HttpClient } from '@angular/common/http';
import { Provider, Type } from '@angular/core';
import { Type } from '@angular/core';

import { SpyObject } from './mock';
import { Token } from './token';

export enum HTTPMethod {
GET = 'GET',
Expand Down Expand Up @@ -48,7 +50,7 @@ export function createHTTPFactory<T>(dataService: Type<T>, providers = []): () =
http.controller = TestBed.get(HttpTestingController);
http.dataService = TestBed.get(dataService);
http.httpClient = TestBed.get(HttpClient);
http.get = function<S>(provider: Type<S>): S & SpyObject<S> {
http.get = function<S>(provider: Token<S>): S & SpyObject<S> {
return TestBed.get(provider);
};

Expand Down
1 change: 1 addition & 0 deletions projects/spectator/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './mock';
export * from './rgb-to-hex';
export * from './service';
export * from './spectator';
export * from './token';
export * from './type-in-element';
export * from './globals';
export * from './mock-component';
Expand Down
18 changes: 10 additions & 8 deletions projects/spectator/src/lib/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement, ElementRef, InjectionToken, Type, ChangeDetectorRef } from '@angular/core';
import { DebugElement, ElementRef, Type, ChangeDetectorRef } from '@angular/core';

import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, dispatchTouchEvent } from './dispatch-events';
import { createMouseEvent } from './event-objects';
import { typeInElement } from './type-in-element';
Expand All @@ -17,6 +18,7 @@ import { Observable } from 'rxjs';
import { SpectatorDebugElementNotFoundError } from './errors';
import { SpyObject } from './mock';
import { DOMSelector } from './dom-selectors';
import { Token } from './token';

declare const require: Function;
const $ = require('jquery');
Expand All @@ -36,7 +38,7 @@ export class Spectator<C> {
* @param type
* @returns
*/
get<T>(type: Type<T> | InjectionToken<T>, fromComponentInjector = false): T & SpyObject<T> {
get<T>(type: Token<T> | Token<any>, fromComponentInjector = false): T & SpyObject<T> {
if (fromComponentInjector) {
return this.debugElement.injector.get(type) as T & SpyObject<T>;
}
Expand Down Expand Up @@ -69,8 +71,8 @@ export class Spectator<C> {
*/
query<R extends Element>(directiveOrSelector: string | DOMSelector): R;
query<R>(directiveOrSelector: Type<R>): R;
query<R>(directiveOrSelector: Type<any>, options: { read: Type<R> }): R;
query<R>(directiveOrSelector: Type<any> | DOMSelector | string, options: { read: Type<R> } = { read: undefined }): R {
query<R>(directiveOrSelector: Type<any>, options: { read: Token<R> }): R;
query<R>(directiveOrSelector: Type<any> | DOMSelector | string, options: { read: Token<R> } = { read: undefined }): R {
try {
return _getChild<R>(this.debugElement)(directiveOrSelector, options);
} catch (err) {
Expand All @@ -90,8 +92,8 @@ export class Spectator<C> {
*/
queryAll<R extends Element[]>(directiveOrSelector: string | DOMSelector): R;
queryAll<R>(directiveOrSelector: Type<R>): R[];
queryAll<R>(directiveOrSelector: Type<any>, options: { read: Type<R> }): R[];
queryAll<R>(directiveOrSelector: Type<any> | DOMSelector | string, options: { read: Type<R> } = { read: undefined }): R[] {
queryAll<R>(directiveOrSelector: Type<any>, options: { read: Token<R> }): R[];
queryAll<R>(directiveOrSelector: Type<any> | DOMSelector | string, options: { read: Token<R> } = { read: undefined }): R[] {
return _getChildren<R>(this.debugElement)(directiveOrSelector, options);
}

Expand All @@ -103,8 +105,8 @@ export class Spectator<C> {
*/
queryLast<R extends Element>(directiveOrSelector: string | DOMSelector): R;
queryLast<R>(directiveOrSelector: Type<R>): R;
queryLast<R>(directiveOrSelector: Type<any>, options: { read: Type<R> }): R;
queryLast<R>(directiveOrSelector: Type<R> | DOMSelector | string, options: { read: Type<R> } = { read: undefined }): R {
queryLast<R>(directiveOrSelector: Type<any>, options: { read: Token<R> }): R;
queryLast<R>(directiveOrSelector: Type<R> | DOMSelector | string, options: { read: Token<R> } = { read: undefined }): R {
const result = _getChildren<R>(this.debugElement)(directiveOrSelector, options);
if (result && result.length) {
return result[result.length - 1];
Expand Down
7 changes: 4 additions & 3 deletions projects/spectator/src/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
*/

import { TestBed, TestModuleMetadata } from '@angular/core/testing';
import { InjectionToken, Type } from '@angular/core';
import { Type } from '@angular/core';
import { mockProvider, SpyObject } from './mock';
import { isType } from './is-type';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { Token } from './token';

export interface SpectatorService<S> {
service: S;

get<T>(token: Type<T> | InjectionToken<T>): T & SpyObject<T>;
get<T>(token: Token<T> | Token<any>): T & SpyObject<T>;
}

export type ServiceParams<S> = TestModuleMetadata & {
Expand Down Expand Up @@ -52,7 +53,7 @@ export function createService<S>(options: ServiceParams<S> | Type<S>): Spectator
get service(): S {
return TestBed.get(service);
},
get<T>(token: Type<T> | InjectionToken<T>): T & SpyObject<T> {
get<T>(token: Token<T> | Token<any>): T & SpyObject<T> {
return TestBed.get(token);
}
};
Expand Down
11 changes: 11 additions & 0 deletions projects/spectator/src/lib/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { InjectionToken } from '@angular/core';

/**
* Type for classes that can be used as tokens for injection. We might be inclined to use Angular's `Type<T>` interface for this, however,
* that interface cannot be used for abstract classes, which are also valid tokens. The `InjectableType<T>` supports both abstract and
* concrete classes and thus is a more accurate definition of a class type which can be used for injection.
*/
export type InjectableType<T> = Function & { prototype: T };

/** Type representing valid typesafe token types for provider binding. */
export type Token<T> = InjectableType<T> | InjectionToken<T>;
153 changes: 153 additions & 0 deletions src/app/injection-tokens.jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ConsumerService } from './consumer.service';
import { createHostComponentFactory, createService, createTestComponentFactory, Spectator } from '@netbasal/spectator/jest';
import { AbstractQueryService, QueryService } from './query.service';
import { InjectionToken } from '@angular/core';
import { ZippyComponent } from './zippy/zippy.component';
import { WidgetService } from './widget.service';

const MY_TOKEN = new InjectionToken<AbstractQueryService>('some-token');

describe('Injection tokens', () => {
describe('with Spectator', () => {
const createComponent = createTestComponentFactory({
component: ZippyComponent,
mocks: [WidgetService],
providers: [
QueryService,
{
provide: AbstractQueryService,
useExisting: QueryService
},
{
provide: MY_TOKEN,
useExisting: QueryService
}
]
});

let spectator: Spectator<ZippyComponent>;

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

it('should get by concrete class', () => {
const service = spectator.get(QueryService);
service.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});

it('should get by abstract class as token', () => {
const service = spectator.get(AbstractQueryService);
service.select(); // should compile

const service2 = spectator.get<QueryService>(AbstractQueryService);
service2.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});

it('should get by injection token', () => {
const service = spectator.get(MY_TOKEN);
service.select(); // should compile

const service2 = spectator.get<QueryService>(MY_TOKEN);
service2.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});
});

describe('with SpectatorWithHost', () => {
const createHost = createHostComponentFactory({
component: ZippyComponent,
mocks: [WidgetService],
providers: [
QueryService,
{
provide: AbstractQueryService,
useExisting: QueryService
},
{
provide: MY_TOKEN,
useExisting: QueryService
}
]
});

let host: Spectator<ZippyComponent>;

beforeEach(() => (host = createHost('<zippy></zippy>')));

it('should get by concrete class', () => {
const service = host.get(QueryService);
service.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});

it('should get by abstract class as token', () => {
const service = host.get(AbstractQueryService);
service.select(); // should compile

const service2 = host.get<QueryService>(AbstractQueryService);
service2.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});

it('should get by injection token', () => {
const service = host.get(MY_TOKEN);
service.select(); // should compile

const service2 = host.get<QueryService>(MY_TOKEN);
service2.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});
});

describe('with Service', () => {
const spectator = createService({
service: ConsumerService,
mocks: [WidgetService],
providers: [
QueryService,
{
provide: AbstractQueryService,
useExisting: QueryService
},
{
provide: MY_TOKEN,
useExisting: QueryService
}
]
});

it('should get by concrete class', () => {
const service = spectator.get(QueryService);
service.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});

it('should get by abstract class as token', () => {
const service = spectator.get(AbstractQueryService);
service.select(); // should compile

const service2 = spectator.get<QueryService>(AbstractQueryService);
service2.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});

it('should get by injection token', () => {
const service = spectator.get(MY_TOKEN);
service.select(); // should compile

const service2 = spectator.get<QueryService>(MY_TOKEN);
service2.selectName(); // should compile

expect(service).toBeInstanceOf(QueryService);
});
});
});

0 comments on commit 877013b

Please sign in to comment.