From f47853eb05fba6b7c16275804ae21302658c2032 Mon Sep 17 00:00:00 2001 From: Michael Gusev Date: Sat, 22 Feb 2020 23:05:33 +0100 Subject: [PATCH] feat: Base class for directives and components Signed-off-by: Michael Gusev --- lib/common/Mock.ts | 33 ++++++++++++++ lib/common/index.ts | 1 + lib/common/mock-of.decorator.ts | 20 ++++++++- lib/mock-component/mock-component.ts | 53 ++++++----------------- lib/mock-directive/mock-directive.ts | 52 +++++++--------------- lib/mock-pipe/mock-pipe.spec.ts | 64 +++++++++++++++++++--------- lib/mock-pipe/mock-pipe.ts | 17 ++++++-- 7 files changed, 138 insertions(+), 102 deletions(-) create mode 100644 lib/common/Mock.ts diff --git a/lib/common/Mock.ts b/lib/common/Mock.ts new file mode 100644 index 0000000000..dac8829714 --- /dev/null +++ b/lib/common/Mock.ts @@ -0,0 +1,33 @@ +import { EventEmitter } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; + +export class Mock implements ControlValueAccessor { + constructor() { + for (const method of (this as any).__mockedMethods) { + if ((this as any)[method]) { + continue; + } + (this as any)[method] = () => undefined; + } + for (const output of (this as any).__mockedOutputs) { + if ((this as any)[output]) { + continue; + } + (this as any)[output] = new EventEmitter(); + } + } + + __simulateChange = (param: any) => {}; // tslint:disable-line:variable-name + + __simulateTouch = () => {}; // tslint:disable-line:variable-name + + registerOnChange(fn: (value: any) => void): void { + this.__simulateChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.__simulateTouch = fn; + } + + writeValue = () => {}; +} diff --git a/lib/common/index.ts b/lib/common/index.ts index 264246fcef..7b81190dfd 100644 --- a/lib/common/index.ts +++ b/lib/common/index.ts @@ -1 +1,2 @@ export * from './mock-of.decorator'; +export * from './Mock'; diff --git a/lib/common/mock-of.decorator.ts b/lib/common/mock-of.decorator.ts index 5a26bf0852..be57aaea5e 100644 --- a/lib/common/mock-of.decorator.ts +++ b/lib/common/mock-of.decorator.ts @@ -7,10 +7,28 @@ import { Type } from '@angular/core'; // Additionally, if we set breakpoints, we can inspect the actual class being mocked // by looking into the 'mockOf' property on the class. /* tslint:disable-next-line variable-name */ -export const MockOf = (mockClass: Type) => (constructor: Type) => { +export const MockOf = (mockClass: Type, outputs?: string[]) => (constructor: Type) => { Object.defineProperties(constructor, { mockOf: {value: mockClass}, name: {value: `MockOf${mockClass.name}`}, nameConstructor: {value: constructor.name}, }); + + const mockedMethods = []; + for (const method of Object.getOwnPropertyNames(mockClass.prototype || {})) { + // Skipping getters and setters + const descriptor = Object.getOwnPropertyDescriptor(mockClass.prototype, method); + const isGetterSetter = descriptor && (descriptor.get || descriptor.set); + if (!isGetterSetter && !constructor.prototype[method]) { + mockedMethods.push(method); + } + } + + const mockedOutputs = []; + for (const output of outputs || []) { + mockedOutputs.push(output.split(':')[0]); + } + + constructor.prototype.__mockedMethods = mockedMethods; + constructor.prototype.__mockedOutputs = mockedOutputs; }; diff --git a/lib/mock-component/mock-component.ts b/lib/mock-component/mock-component.ts index a4e3ab0439..f6e498c83a 100644 --- a/lib/mock-component/mock-component.ts +++ b/lib/mock-component/mock-component.ts @@ -2,7 +2,6 @@ import { core } from '@angular/compiler'; import { ChangeDetectorRef, Component, - EventEmitter, forwardRef, Query, TemplateRef, @@ -10,34 +9,34 @@ import { ViewChild, ViewContainerRef, } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { staticFalse } from '../../tests'; -import { MockOf } from '../common'; +import { Mock, MockOf } from '../common'; import { directiveResolver } from '../common/reflect'; -const cache = new Map, Type>(); +const cache = new Map, Type>>(); -export type MockedComponent = T & { +export type MockedComponent = T & Mock & { /** Helper function to hide rendered @ContentChild() template. */ __hide(contentChildSelector: string): void; /** Helper function to render any @ContentChild() template with any context. */ __render(contentChildSelector: string, $implicit?: any, variables?: {[key: string]: any}): void; - - __simulateChange(value: any): void; - __simulateTouch(): void; }; export function MockComponents(...components: Array>): Array> { return components.map((component) => MockComponent(component, undefined)); } -export function MockComponent(component: Type, metaData?: core.Directive): Type { +export function MockComponent( + component: Type, + metaData?: core.Directive, +): Type> { const cacheHit = cache.get(component); if (cacheHit) { - return cacheHit as Type; + return cacheHit as Type>; } const { exportAs, inputs, outputs, queries, selector } = metaData || directiveResolver.resolve(component); @@ -92,21 +91,10 @@ export function MockComponent(component: Type, metaData? template, }; - @MockOf(component) - class ComponentMock implements ControlValueAccessor { + @MockOf(component, outputs) + class ComponentMock extends Mock { constructor(changeDetector: ChangeDetectorRef) { - Object.getOwnPropertyNames(component.prototype).forEach((method) => { - // Skipping getters and setters - const descriptor = Object.getOwnPropertyDescriptor(component.prototype, method); - const isGetterSetter = descriptor && (descriptor.get || descriptor.set); - if (!isGetterSetter && !(this as any)[method]) { - (this as any)[method] = () => {}; - } - }); - - (options.outputs || []).forEach((output) => { - (this as any)[output.split(':')[0]] = new EventEmitter(); - }); + super(); // Providing method to hide any @ContentChild based on its selector. (this as any).__hide = (contentChildSelector: string) => { @@ -134,24 +122,9 @@ export function MockComponent(component: Type, metaData? } }; } - - __simulateChange = (param: any) => {}; // tslint:disable-line:variable-name - __simulateTouch = () => {}; // tslint:disable-line:variable-name - - registerOnChange(fn: (value: any) => void): void { - this.__simulateChange = fn; - } - - registerOnTouched(fn: () => void): void { - this.__simulateTouch = fn; - } - - writeValue = (value: any) => {}; } - // tslint:disable-next-line:no-angle-bracket-type-assertion - const mockedComponent = Component(options)( ComponentMock as Type); - + const mockedComponent: Type> = Component(options)(ComponentMock as any); cache.set(component, mockedComponent); return mockedComponent; diff --git a/lib/mock-directive/mock-directive.ts b/lib/mock-directive/mock-directive.ts index 200e23e831..6ea5ba3b3a 100644 --- a/lib/mock-directive/mock-directive.ts +++ b/lib/mock-directive/mock-directive.ts @@ -1,20 +1,11 @@ -import { - Directive, - ElementRef, - EventEmitter, - forwardRef, - Optional, - TemplateRef, - Type, - ViewContainerRef -} from '@angular/core'; - -import { MockOf } from '../common'; +import { Directive, ElementRef, forwardRef, Optional, TemplateRef, Type, ViewContainerRef } from '@angular/core'; + +import { Mock, MockOf } from '../common'; import { directiveResolver } from '../common/reflect'; -const cache = new Map, Type>(); +const cache = new Map, Type>>(); -export type MockedDirective = T & { +export type MockedDirective = T & Mock & { /** Pointer to current element in case of Attribute Directives. */ __element?: ElementRef; @@ -43,9 +34,7 @@ export function MockDirective(directive: Type): Type(directive: Type): Type DirectiveMock) }], - selector - }) - class DirectiveMock { + selector, + }; + @MockOf(directive, outputs) + class DirectiveMock extends Mock { constructor( @Optional() element?: ElementRef, @Optional() template?: TemplateRef, @Optional() viewContainer?: ViewContainerRef, ) { - (this as any).__element = element; + super(); // Basically any directive on ng-template is treated as structural, even it doesn't control render process. // In our case we don't if we should render it or not and due to this we do nothing. + (this as any).__element = element; (this as any).__template = template; (this as any).__viewContainer = viewContainer; (this as any).__isStructural = template && viewContainer; - Object.getOwnPropertyNames(directive.prototype).forEach((method) => { - // Skipping getters and setters - const descriptor = Object.getOwnPropertyDescriptor(directive.prototype, method); - const isGetterSetter = descriptor && (descriptor.get || descriptor.set); - if (!isGetterSetter && !(this as any)[method]) { - (this as any)[method] = () => {}; - } - }); - - (outputs || []).forEach((output) => { - (this as any)[output.split(':')[0]] = new EventEmitter(); - }); - // Providing method to render mocked values. (this as any).__render = ($implicit?: any, variables?: {[key: string]: any}) => { if (viewContainer && template) { @@ -92,9 +70,9 @@ export function MockDirective(directive: Type): Type> = Directive(options)(DirectiveMock as any); + cache.set(directive, mockedDirective); - return DirectiveMock as Type>; + return mockedDirective; } diff --git a/lib/mock-pipe/mock-pipe.spec.ts b/lib/mock-pipe/mock-pipe.spec.ts index 1353a71bd4..da28e6d900 100644 --- a/lib/mock-pipe/mock-pipe.spec.ts +++ b/lib/mock-pipe/mock-pipe.spec.ts @@ -32,29 +32,53 @@ export class ExampleComponent { describe('MockPipe', () => { let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - ExampleComponent, - MockPipe(ExamplePipe, () => 'foo'), - MockPipe(AnotherExamplePipe) - ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ExampleComponent); - fixture.detectChanges(); - }); + describe('Base tests', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ExampleComponent, + MockPipe(ExamplePipe, () => 'foo'), + MockPipe(AnotherExamplePipe) + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExampleComponent); + fixture.detectChanges(); + }); + + it('should not display the word hi that is output by the unmocked pipe, because it is now mocked', () => { + expect(fixture.debugElement.query(By.css('#anotherExamplePipe')).nativeElement.innerHTML).toEqual(''); + }); - it('should not display the word hi that is output by the unmocked pipe, because it is now mocked', () => { - expect(fixture.debugElement.query(By.css('#anotherExamplePipe')).nativeElement.innerHTML).toEqual(''); + describe('with transform override', () => { + it('should return the result of the provided transform function', () => { + expect(fixture.debugElement.query(By.css('#examplePipe')).nativeElement.innerHTML).toEqual('foo'); + }); + }); }); - describe('with transform override', () => { - it('should return the result of the provided transform function', () => { - expect(fixture.debugElement.query(By.css('#examplePipe')).nativeElement.innerHTML).toEqual('foo'); + describe('Cache check', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ExampleComponent, + MockPipe(ExamplePipe, () => 'bar'), + MockPipe(AnotherExamplePipe) + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExampleComponent); + fixture.detectChanges(); + }); + + it('should return the result of the new provided transform function', () => { + expect(fixture.debugElement.query(By.css('#examplePipe')).nativeElement.innerHTML).toEqual('bar'); }); }); }); diff --git a/lib/mock-pipe/mock-pipe.ts b/lib/mock-pipe/mock-pipe.ts index 4b9f854958..07b166b303 100644 --- a/lib/mock-pipe/mock-pipe.ts +++ b/lib/mock-pipe/mock-pipe.ts @@ -3,20 +3,29 @@ import { Pipe, PipeTransform, Type } from '@angular/core'; import { MockOf } from '../common'; import { pipeResolver } from '../common/reflect'; +export type MockedPipe = T; + export function MockPipes(...pipes: Array>): Array> { return pipes.map((pipe) => MockPipe(pipe, undefined)); } const defaultTransform = (...args: any[]): void => undefined; -export function MockPipe(pipe: Type, - transform: TPipe['transform'] = defaultTransform): Type { - const pipeName = pipeResolver.resolve(pipe).name; +export function MockPipe( + pipe: Type, + transform: TPipe['transform'] = defaultTransform, +): Type> { + const { name } = pipeResolver.resolve(pipe); + + const options: Pipe = { + name, + }; @MockOf(pipe) class PipeMock implements PipeTransform { transform = transform || defaultTransform; } - const mockedPipe = Pipe({ name: pipeName })(PipeMock as Type); + const mockedPipe: Type = Pipe(options)(PipeMock as any); + return mockedPipe; }