Created
January 21, 2019 15:25
-
-
Save peerhenry/a8dd01226114758936aef009bd95527f to your computer and use it in GitHub Desktop.
a script that provides easy mocking of components, pipes and services
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Type, Component, Pipe, EventEmitter } from '@angular/core'; | |
| const ANNOTATIONS = '__annotations__'; | |
| const PARAMETERS = '__parameters__'; | |
| const PROP_METADATA = '__prop__metadata__'; | |
| export type Mocked<T> = { [P in keyof T]: T[P] }; | |
| type Constructor<T> = Function & { prototype: T }; | |
| export function mockType<T>(type: Constructor<T>, overrides: Partial<T> = {}): Constructor<Mocked<T>> { | |
| const mock = class {}; | |
| Object.defineProperty(mock, 'name', { value: `Mock${type.name}`}); | |
| const originalProperties = Object.getOwnPropertyDescriptors(type.prototype); | |
| const targetProperties: { [x: string]: PropertyDescriptor } = {}; | |
| for (const propertyName of Object.keys(originalProperties)) { | |
| const originalProperty = originalProperties[propertyName]; | |
| if (originalProperty.enumerable === false) { | |
| continue; | |
| } | |
| const targetProperty: PropertyDescriptor = convertProperty(originalProperty); | |
| targetProperties[propertyName] = targetProperty; | |
| } | |
| for (const propertyName of Object.keys(overrides)) { | |
| targetProperties[propertyName] = { | |
| configurable: true, | |
| enumerable: true, | |
| value: overrides[propertyName] | |
| }; | |
| } | |
| Object.defineProperties(mock.prototype, targetProperties); | |
| return <Type<Mocked<T>>>mock; | |
| } | |
| function convertProperty(originalProperty: TypedPropertyDescriptor<any> & PropertyDescriptor) { | |
| const targetProperty: PropertyDescriptor = { enumerable: true, configurable: true }; | |
| if ('value' in originalProperty) { | |
| originalProperty.writable = originalProperty.writable; | |
| if (originalProperty.value instanceof Function) { | |
| targetProperty.value = () => { }; | |
| } else { | |
| targetProperty.value = originalProperty.value; | |
| } | |
| } else { | |
| let storage; | |
| targetProperty.get = () => storage; | |
| if ('set' in originalProperty) { | |
| targetProperty.set = (value: any) => storage = value; | |
| } | |
| } | |
| return targetProperty; | |
| } | |
| interface ProvideType<T> { provide: Constructor<T>; useClass: Constructor<Mocked<T>>; } | |
| export function mockProvide<T>(type: Constructor<T>, overrides: Partial<T> = {}): ProvideType<T> { | |
| return { | |
| provide: type, | |
| useClass: mockType(type, overrides) | |
| }; | |
| } | |
| export const provideMocked = mockProvide; | |
| export function mockComponent<T>(type: Constructor<T>, overrides: Partial<T> = {}): Constructor<Mocked<T>> { | |
| if (!(ANNOTATIONS in type)) { | |
| throw new Error('Cannot decorate non-component'); | |
| } | |
| const component = <Component>(type as any)[ANNOTATIONS][0]; | |
| return this.mockClassToComponent({ | |
| selector: component.selector, | |
| providers: component.providers | |
| }, type, overrides); | |
| } | |
| type CArg = Component & { selector: string }; | |
| export function mockClassToComponent<T>(selector: string, type: Constructor<T>, overrides?: Partial<T>): Constructor<Mocked<T>>; | |
| // tslint:disable-next-line:unified-signatures | |
| export function mockClassToComponent<T>(component: CArg, type: Constructor<T>, overrides?: Partial<T>): Constructor<Mocked<T>>; | |
| export function mockClassToComponent<T>(arg: string | CArg, type: Constructor<T>, overrides: Partial<T> = {}): Constructor<Mocked<T>> { | |
| if (typeof arg === 'string') { | |
| arg = { selector: arg }; | |
| } | |
| const mock = mockType(type, overrides); | |
| const componentMock = <Constructor<Mocked<T>>><any>Component({ | |
| template: `<p>mocked component with selector ${arg.selector}</p>`, | |
| ...arg, | |
| })(mock); | |
| const incognito = (componentMock as any); | |
| incognito[PROP_METADATA] = (type as any)[PROP_METADATA]; | |
| incognito.ngBaseDef = (type as any).ngBaseDef; | |
| if (incognito.ngBaseDef) { | |
| for (const key of Object.keys(incognito.ngBaseDef.outputs)) { | |
| Object.defineProperty(componentMock.prototype, key, { | |
| value: new EventEmitter() | |
| }); | |
| } | |
| } | |
| return incognito; | |
| } | |
| export function mockPipe<T>(type: Constructor<T>, overrides: Partial<T> = {}): Constructor<Mocked<T>> { | |
| if (!(ANNOTATIONS in type)) { | |
| throw new Error('Cannot decorate non-decorated pipe'); | |
| } | |
| const pipe = <Pipe>(type as any)[ANNOTATIONS][0]; | |
| const name = pipe.name; | |
| return this.mockClassToPipe(name, type); | |
| } | |
| export function mockClassToPipe<T>(name: string, type: Constructor<T>, overrides: Partial<T> = {}): Constructor<Mocked<T>> { | |
| const mock = mockType(type, overrides); | |
| const pipeMock = <Constructor<Mocked<T>>><any>Pipe({ | |
| name: name | |
| })(mock); | |
| (pipeMock as any)[PROP_METADATA] = (type as any)[PROP_METADATA]; | |
| (pipeMock as any).ngBaseDef = (type as any).ngBaseDef; | |
| return pipeMock; | |
| } | |
| // todo: mock directive |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment