Skip to content

Commit

Permalink
feat: Base class for directives and components
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Gusev <m@sudo.eu>
  • Loading branch information
satanTime committed Mar 14, 2020
1 parent daaa204 commit f47853e
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 102 deletions.
33 changes: 33 additions & 0 deletions lib/common/Mock.ts
Original file line number Diff line number Diff line change
@@ -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<any>();
}
}

__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 = () => {};
}
1 change: 1 addition & 0 deletions lib/common/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './mock-of.decorator';
export * from './Mock';
20 changes: 19 additions & 1 deletion lib/common/mock-of.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>) => (constructor: Type<any>) => {
export const MockOf = (mockClass: Type<any>, outputs?: string[]) => (constructor: Type<any>) => {
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;
};
53 changes: 13 additions & 40 deletions lib/mock-component/mock-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,41 @@ import { core } from '@angular/compiler';
import {
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
Query,
TemplateRef,
Type,
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<Component>, Type<Component>>();
const cache = new Map<Type<Component>, Type<MockedComponent<Component>>>();

export type MockedComponent<T> = T & {
export type MockedComponent<T> = 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<Type<any>>): Array<Type<any>> {
return components.map((component) => MockComponent(component, undefined));
}

export function MockComponent<TComponent>(component: Type<TComponent>, metaData?: core.Directive): Type<TComponent> {
export function MockComponent<TComponent>(
component: Type<TComponent>,
metaData?: core.Directive,
): Type<MockedComponent<TComponent>> {
const cacheHit = cache.get(component);
if (cacheHit) {
return cacheHit as Type<TComponent>;
return cacheHit as Type<MockedComponent<TComponent>>;
}

const { exportAs, inputs, outputs, queries, selector } = metaData || directiveResolver.resolve(component);
Expand Down Expand Up @@ -92,21 +91,10 @@ export function MockComponent<TComponent>(component: Type<TComponent>, 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<any>();
});
super();

// Providing method to hide any @ContentChild based on its selector.
(this as any).__hide = (contentChildSelector: string) => {
Expand Down Expand Up @@ -134,24 +122,9 @@ export function MockComponent<TComponent>(component: Type<TComponent>, 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)(<any> ComponentMock as Type<TComponent>);

const mockedComponent: Type<MockedComponent<TComponent>> = Component(options)(ComponentMock as any);
cache.set(component, mockedComponent);

return mockedComponent;
Expand Down
52 changes: 15 additions & 37 deletions lib/mock-directive/mock-directive.ts
Original file line number Diff line number Diff line change
@@ -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<Directive>, Type<Directive>>();
const cache = new Map<Type<Directive>, Type<MockedDirective<Directive>>>();

export type MockedDirective<T> = T & {
export type MockedDirective<T> = T & Mock & {
/** Pointer to current element in case of Attribute Directives. */
__element?: ElementRef;

Expand Down Expand Up @@ -43,46 +34,33 @@ export function MockDirective<TDirective>(directive: Type<TDirective>): Type<Moc

const { selector, exportAs, inputs, outputs } = directiveResolver.resolve(directive);

// tslint:disable:no-unnecessary-class
@MockOf(directive)
@Directive({
const options: Directive = {
exportAs,
inputs,
outputs,
providers: [{
provide: directive,
useExisting: forwardRef(() => DirectiveMock)
}],
selector
})
class DirectiveMock {
selector,
};

@MockOf(directive, outputs)
class DirectiveMock extends Mock {
constructor(
@Optional() element?: ElementRef,
@Optional() template?: TemplateRef<any>,
@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<any>();
});

// Providing method to render mocked values.
(this as any).__render = ($implicit?: any, variables?: {[key: string]: any}) => {
if (viewContainer && template) {
Expand All @@ -92,9 +70,9 @@ export function MockDirective<TDirective>(directive: Type<TDirective>): Type<Moc
};
}
}
// tslint:enable:no-unnecessary-class

cache.set(directive, DirectiveMock);
const mockedDirective: Type<MockedDirective<TDirective>> = Directive(options)(DirectiveMock as any);
cache.set(directive, mockedDirective);

return DirectiveMock as Type<MockedDirective<TDirective>>;
return mockedDirective;
}
64 changes: 44 additions & 20 deletions lib/mock-pipe/mock-pipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,53 @@ export class ExampleComponent {
describe('MockPipe', () => {
let fixture: ComponentFixture<ExampleComponent>;

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');
});
});
});
17 changes: 13 additions & 4 deletions lib/mock-pipe/mock-pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@ import { Pipe, PipeTransform, Type } from '@angular/core';
import { MockOf } from '../common';
import { pipeResolver } from '../common/reflect';

export type MockedPipe<T> = T;

export function MockPipes(...pipes: Array<Type<PipeTransform>>): Array<Type<PipeTransform>> {
return pipes.map((pipe) => MockPipe(pipe, undefined));
}

const defaultTransform = (...args: any[]): void => undefined;
export function MockPipe<TPipe extends PipeTransform>(pipe: Type<TPipe>,
transform: TPipe['transform'] = defaultTransform): Type<TPipe> {
const pipeName = pipeResolver.resolve(pipe).name;
export function MockPipe<TPipe extends PipeTransform>(
pipe: Type<TPipe>,
transform: TPipe['transform'] = defaultTransform,
): Type<MockedPipe<TPipe>> {
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<TPipe>);
const mockedPipe: Type<TPipe> = Pipe(options)(PipeMock as any);

return mockedPipe;
}

0 comments on commit f47853e

Please sign in to comment.