From 877013b2ffe6605fd6ef7a9b4e3d4b44f5975a05 Mon Sep 17 00:00:00 2001 From: Dirk Luijk Date: Fri, 2 Aug 2019 17:27:20 +0200 Subject: [PATCH] fix(spectator): improve type inference even more --- projects/spectator/jest/src/host.ts | 6 +- projects/spectator/jest/src/internals.ts | 5 +- projects/spectator/jest/src/service.ts | 6 +- projects/spectator/src/lib/host.ts | 8 +- projects/spectator/src/lib/http.ts | 6 +- projects/spectator/src/lib/index.ts | 1 + projects/spectator/src/lib/internals.ts | 18 +-- projects/spectator/src/lib/service.ts | 7 +- projects/spectator/src/lib/token.ts | 11 ++ src/app/injection-tokens.jest.ts | 153 +++++++++++++++++++++++ src/app/injection-tokens.spec.ts | 153 +++++++++++++++++++++++ src/app/query.service.ts | 11 +- 12 files changed, 359 insertions(+), 26 deletions(-) create mode 100644 projects/spectator/src/lib/token.ts create mode 100644 src/app/injection-tokens.jest.ts create mode 100644 src/app/injection-tokens.spec.ts diff --git a/projects/spectator/jest/src/host.ts b/projects/spectator/jest/src/host.ts index b509fd80..42e6d5e4 100644 --- a/projects/spectator/jest/src/host.ts +++ b/projects/spectator/jest/src/host.ts @@ -6,8 +6,8 @@ * 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'; @@ -15,7 +15,7 @@ export class SpectatorWithHost extends BaseSpectatorWithHo /** * @inheritDoc */ - get(type: Type | InjectionToken, fromComponentInjector = false): T & SpyObject { + get(type: Token, fromComponentInjector = false): T & SpyObject { return super.get(type, fromComponentInjector) as T & SpyObject; } } diff --git a/projects/spectator/jest/src/internals.ts b/projects/spectator/jest/src/internals.ts index 939aaa3a..4d403b75 100644 --- a/projects/spectator/jest/src/internals.ts +++ b/projects/spectator/jest/src/internals.ts @@ -6,8 +6,7 @@ * 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'; @@ -15,7 +14,7 @@ export class Spectator extends BaseSpectator { /** * @inheritDoc */ - get(type: Type | InjectionToken, fromComponentInjector = false): T & SpyObject { + get(type: Token | Token, fromComponentInjector = false): T & SpyObject { return super.get(type, fromComponentInjector) as T & SpyObject; } } diff --git a/projects/spectator/jest/src/service.ts b/projects/spectator/jest/src/service.ts index 297facea..b7d05642 100644 --- a/projects/spectator/jest/src/service.ts +++ b/projects/spectator/jest/src/service.ts @@ -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 extends BaseSpectatorService { - get(token: Type | InjectionToken): T & SpyObject; + get(token: Token | Token): T & SpyObject; } export function createService(options: ServiceParams | Type): SpectatorService { diff --git a/projects/spectator/src/lib/host.ts b/projects/spectator/src/lib/host.ts index 34f0e342..982d68dd 100644 --- a/projects/spectator/src/lib/host.ts +++ b/projects/spectator/src/lib/host.ts @@ -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'; @@ -44,7 +46,8 @@ export class SpectatorWithHost extends Spectator { * @returns */ queryHost(directiveOrSelector: string): T; - queryHost(directiveOrSelector: Type, options?: { read }): T; + queryHost(directiveOrSelector: Type): T; + queryHost(directiveOrSelector: Type, options: { read: Token }): T; queryHost(directiveOrSelector: Type | string, options: { read } = { read: undefined }): T { return _getChild(this.hostDebugElement)(directiveOrSelector, options); } @@ -56,7 +59,8 @@ export class SpectatorWithHost extends Spectator { * @returns */ queryHostAll(directiveOrSelector: string): T[]; - queryHostAll(directiveOrSelector: Type, options?: { read }): T[]; + queryHostAll(directiveOrSelector: Type): T[]; + queryHostAll(directiveOrSelector: Type, options: { read: Token }): T[]; queryHostAll(directiveOrSelector: Type | string, options: { read } = { read: undefined }): T[] { return _getChildren(this.hostDebugElement)(directiveOrSelector, options); } diff --git a/projects/spectator/src/lib/http.ts b/projects/spectator/src/lib/http.ts index 3ffd2b2f..fac333ae 100644 --- a/projects/spectator/src/lib/http.ts +++ b/projects/spectator/src/lib/http.ts @@ -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', @@ -48,7 +50,7 @@ export function createHTTPFactory(dataService: Type, providers = []): () = http.controller = TestBed.get(HttpTestingController); http.dataService = TestBed.get(dataService); http.httpClient = TestBed.get(HttpClient); - http.get = function(provider: Type): S & SpyObject { + http.get = function(provider: Token): S & SpyObject { return TestBed.get(provider); }; diff --git a/projects/spectator/src/lib/index.ts b/projects/spectator/src/lib/index.ts index 1bb44a9b..56f54094 100644 --- a/projects/spectator/src/lib/index.ts +++ b/projects/spectator/src/lib/index.ts @@ -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'; diff --git a/projects/spectator/src/lib/internals.ts b/projects/spectator/src/lib/internals.ts index 4192533e..9afd1363 100644 --- a/projects/spectator/src/lib/internals.ts +++ b/projects/spectator/src/lib/internals.ts @@ -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'; @@ -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'); @@ -36,7 +38,7 @@ export class Spectator { * @param type * @returns */ - get(type: Type | InjectionToken, fromComponentInjector = false): T & SpyObject { + get(type: Token | Token, fromComponentInjector = false): T & SpyObject { if (fromComponentInjector) { return this.debugElement.injector.get(type) as T & SpyObject; } @@ -69,8 +71,8 @@ export class Spectator { */ query(directiveOrSelector: string | DOMSelector): R; query(directiveOrSelector: Type): R; - query(directiveOrSelector: Type, options: { read: Type }): R; - query(directiveOrSelector: Type | DOMSelector | string, options: { read: Type } = { read: undefined }): R { + query(directiveOrSelector: Type, options: { read: Token }): R; + query(directiveOrSelector: Type | DOMSelector | string, options: { read: Token } = { read: undefined }): R { try { return _getChild(this.debugElement)(directiveOrSelector, options); } catch (err) { @@ -90,8 +92,8 @@ export class Spectator { */ queryAll(directiveOrSelector: string | DOMSelector): R; queryAll(directiveOrSelector: Type): R[]; - queryAll(directiveOrSelector: Type, options: { read: Type }): R[]; - queryAll(directiveOrSelector: Type | DOMSelector | string, options: { read: Type } = { read: undefined }): R[] { + queryAll(directiveOrSelector: Type, options: { read: Token }): R[]; + queryAll(directiveOrSelector: Type | DOMSelector | string, options: { read: Token } = { read: undefined }): R[] { return _getChildren(this.debugElement)(directiveOrSelector, options); } @@ -103,8 +105,8 @@ export class Spectator { */ queryLast(directiveOrSelector: string | DOMSelector): R; queryLast(directiveOrSelector: Type): R; - queryLast(directiveOrSelector: Type, options: { read: Type }): R; - queryLast(directiveOrSelector: Type | DOMSelector | string, options: { read: Type } = { read: undefined }): R { + queryLast(directiveOrSelector: Type, options: { read: Token }): R; + queryLast(directiveOrSelector: Type | DOMSelector | string, options: { read: Token } = { read: undefined }): R { const result = _getChildren(this.debugElement)(directiveOrSelector, options); if (result && result.length) { return result[result.length - 1]; diff --git a/projects/spectator/src/lib/service.ts b/projects/spectator/src/lib/service.ts index 5eeff471..ffaa8db7 100644 --- a/projects/spectator/src/lib/service.ts +++ b/projects/spectator/src/lib/service.ts @@ -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 { service: S; - get(token: Type | InjectionToken): T & SpyObject; + get(token: Token | Token): T & SpyObject; } export type ServiceParams = TestModuleMetadata & { @@ -52,7 +53,7 @@ export function createService(options: ServiceParams | Type): Spectator get service(): S { return TestBed.get(service); }, - get(token: Type | InjectionToken): T & SpyObject { + get(token: Token | Token): T & SpyObject { return TestBed.get(token); } }; diff --git a/projects/spectator/src/lib/token.ts b/projects/spectator/src/lib/token.ts new file mode 100644 index 00000000..7b2ed4d4 --- /dev/null +++ b/projects/spectator/src/lib/token.ts @@ -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` interface for this, however, + * that interface cannot be used for abstract classes, which are also valid tokens. The `InjectableType` 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 = Function & { prototype: T }; + +/** Type representing valid typesafe token types for provider binding. */ +export type Token = InjectableType | InjectionToken; diff --git a/src/app/injection-tokens.jest.ts b/src/app/injection-tokens.jest.ts new file mode 100644 index 00000000..e484ec65 --- /dev/null +++ b/src/app/injection-tokens.jest.ts @@ -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('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; + + 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(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(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; + + beforeEach(() => (host = createHost(''))); + + 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(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(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(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(MY_TOKEN); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + }); + }); +}); diff --git a/src/app/injection-tokens.spec.ts b/src/app/injection-tokens.spec.ts new file mode 100644 index 00000000..5728d952 --- /dev/null +++ b/src/app/injection-tokens.spec.ts @@ -0,0 +1,153 @@ +import { ConsumerService } from './consumer.service'; +import { createHostComponentFactory, createService, createTestComponentFactory, Spectator } from '@netbasal/spectator'; +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('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; + + beforeEach(() => (spectator = createComponent())); + + it('should get by concrete class', () => { + const service = spectator.get(QueryService); + service.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + + it('should get by abstract class as token', () => { + const service = spectator.get(AbstractQueryService); + service.select(); // should compile + + const service2 = spectator.get(AbstractQueryService); + service2.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + + it('should get by injection token', () => { + const service = spectator.get(MY_TOKEN); + service.select(); // should compile + + const service2 = spectator.get(MY_TOKEN); + service2.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + }); + + describe('with SpectatorWithHost', () => { + const createHost = createHostComponentFactory({ + component: ZippyComponent, + mocks: [WidgetService], + providers: [ + QueryService, + { + provide: AbstractQueryService, + useExisting: QueryService + }, + { + provide: MY_TOKEN, + useExisting: QueryService + } + ] + }); + + let host: Spectator; + + beforeEach(() => (host = createHost(''))); + + it('should get by concrete class', () => { + const service = host.get(QueryService); + service.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + + it('should get by abstract class as token', () => { + const service = host.get(AbstractQueryService); + service.select(); // should compile + + const service2 = host.get(AbstractQueryService); + service2.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + + it('should get by injection token', () => { + const service = host.get(MY_TOKEN); + service.select(); // should compile + + const service2 = host.get(MY_TOKEN); + service2.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + }); + + 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 instanceof QueryService).toBe(true); + }); + + it('should get by abstract class as token', () => { + const service = spectator.get(AbstractQueryService); + service.select(); // should compile + + const service2 = spectator.get(AbstractQueryService); + service2.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + + it('should get by injection token', () => { + const service = spectator.get(MY_TOKEN); + service.select(); // should compile + + const service2 = spectator.get(MY_TOKEN); + service2.selectName(); // should compile + + expect(service instanceof QueryService).toBe(true); + }); + }); +}); diff --git a/src/app/query.service.ts b/src/app/query.service.ts index 24256087..cb66e1e4 100644 --- a/src/app/query.service.ts +++ b/src/app/query.service.ts @@ -1,10 +1,17 @@ import { Injectable } from '@angular/core'; import { of } from 'rxjs'; import { WidgetService } from './widget.service'; +import { Observable } from 'rxjs/internal/Observable'; + +export abstract class AbstractQueryService { + abstract select(): Observable; +} @Injectable() -export class QueryService { - constructor(private service: WidgetService) {} +export class QueryService extends AbstractQueryService { + constructor(private service: WidgetService) { + super(); + } selectName() { return of('Netanel');