Skip to content

Commit

Permalink
feat: ease of getting inputs and outputs
Browse files Browse the repository at this point in the history
MockHelper.getInput
MockHelper.getInputOrFail
MockHelper.getOutput
MockHelper.getOutputOrFail

closes #129
  • Loading branch information
satanTime committed May 23, 2020
1 parent cb0bffb commit af9a846
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 18 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,12 @@ MockHelper provides functions to get attribute and structural directives from an
* findOrFail
* findAll

- mockService
- getInput
- getInputOrFail
- getOutput
- getOutputOrFail

* mockService

```typescript
// returns attribute or structural directive
Expand Down Expand Up @@ -489,6 +494,15 @@ const component: MockedDebugElement<Component> = MockHelper.findOrFail(fixture.d
const component: MockedDebugElement<Component> = MockHelper.findOrFail(fixture.debugElement, 'div.container');
```

To avoid pain of knowing a name of a component or a directive what an input or an output belongs to, you can use next functions:

```typescript
const inputValue: number | undefined = MockHelper.getInput(debugElement, 'param1');
const inputValue: number = MockHelper.getInputOrFail(debugElement, 'param1');
const outputValue: EventEmitter<any> | undefined = MockHelper.getOutput(debugElement, 'update');
const outputValue: EventEmitter<any> = MockHelper.getOutputOrFail(debugElement, 'update');
```

In case if we want to mock methods / properties of a service / provider.

```typescript
Expand All @@ -498,6 +512,12 @@ const spy: Spy = MockHelper.mockService(instance, methodName);
// returns a mocked function / spy of the property. If the property hasn't been mocked yet - mocks it.
const spyGet: Spy = MockHelper.mockService(instance, propertyName, 'get');
const spySet: Spy = MockHelper.mockService(instance, propertyName, 'set');

// or add / override properties and methods.
MockHelper.mockService(instance, {
newPropert: true,
existingMethod: jasmine.createSpy(),
});
```

```typescript
Expand Down
33 changes: 33 additions & 0 deletions e2e/get-inputs-and-outputs/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Component, Directive, EventEmitter, Input, NgModule, Output } from '@angular/core';

@Component({
selector: 'target',
template: '<a (click)="output.emit()">{{ input }}</a>',
})
export class TargetComponent {
@Input('input1') input: string;
@Output('output1') output = new EventEmitter();
}

@Directive({
selector: 'target',
})
export class Target2Directive {
@Input('input2') input: string;
@Input('inputUnused') input2: undefined;
@Output('output2') output = new EventEmitter();
}

@Directive({
selector: 'target',
})
export class Target3Directive {
@Input('input3') input: string;
@Output('output3') output = new EventEmitter();
}

@NgModule({
declarations: [TargetComponent, Target2Directive, Target3Directive],
exports: [TargetComponent, Target2Directive, Target3Directive],
})
export class TargetModule {}
74 changes: 74 additions & 0 deletions e2e/get-inputs-and-outputs/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { TestBed } from '@angular/core/testing';

import { MockHelper } from '../../lib/mock-helper';
import { MockRender } from '../../lib/mock-render';

import { Target2Directive, Target3Directive, TargetComponent, TargetModule } from './fixtures';

describe('get-inputs-and-outputs', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [TargetModule],
}).compileComponents()
);

it('finds them correctly', () => {
const params = {
input1: '1',
input2: '2',
output1: jasmine.createSpy('output1'),
output2: jasmine.createSpy('output2'),
output3: jasmine.createSpy('output3'),
};
const fixture = MockRender<TargetComponent, typeof params>(
`<target
[input1]="input1"
[input2]="input2"
input3="3"
(output1)="output1($event)"
(output2)="output2($event)"
(output3)="output3($event)"
></target>`,
params
);

const componentElement = fixture.point;
const component = fixture.point.componentInstance;
const directive2 = MockHelper.getDirectiveOrFail(componentElement, Target2Directive);
const directive3 = MockHelper.getDirectiveOrFail(componentElement, Target3Directive);

expect(component.input).toEqual('1');
params.output1.calls.reset();
component.output.emit();
expect(params.output1).toHaveBeenCalled();

expect(directive2.input).toEqual('2');
expect(directive2.input2).toEqual(undefined);
params.output2.calls.reset();
directive2.output.emit();
expect(params.output2).toHaveBeenCalled();

expect(directive3.input).toEqual('3');
params.output3.calls.reset();
directive3.output.emit();
expect(params.output3).toHaveBeenCalled();

// a really simple wait that allows us to skip pain of knowing directives.
expect(MockHelper.getInputOrFail(componentElement, 'input1')).toEqual('1');
expect(MockHelper.getInputOrFail(componentElement, 'input2')).toEqual('2');
expect(MockHelper.getInputOrFail(componentElement, 'inputUnused')).toEqual(undefined);
expect(() => MockHelper.getInputOrFail(componentElement, 'inputUndefined')).toThrowError(
'Cannot find inputUndefined input via MockHelper.getInputOrFail'
);
expect(MockHelper.getInputOrFail(componentElement, 'input3')).toEqual('3');
params.output1.calls.reset();
MockHelper.getOutputOrFail(componentElement, 'output1').emit();
expect(params.output1).toHaveBeenCalled();
params.output2.calls.reset();
MockHelper.getOutputOrFail(componentElement, 'output2').emit();
expect(params.output2).toHaveBeenCalled();
params.output3.calls.reset();
MockHelper.getOutputOrFail(componentElement, 'output3').emit();
expect(params.output3).toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions e2e/spies/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('spies:manual-mock', () => {
expect(targetService.echo).toHaveBeenCalledTimes(1);
expect(targetService.echo).toHaveBeenCalledWith('constructor');
expect(component.echo()).toEqual('fake');
expect(targetService.echo).toHaveBeenCalledTimes(2); // tslint:disable-line:no-magic-numbers
expect(targetService.echo).toHaveBeenCalledTimes(2);
}));
});

Expand All @@ -68,6 +68,6 @@ describe('spies:auto-mock', () => {
expect(targetService.echo).toHaveBeenCalledWith('constructor');
(targetService.echo as Spy).and.returnValue('faked');
expect(component.echo()).toEqual('faked');
expect(targetService.echo).toHaveBeenCalledTimes(2); // tslint:disable-line:no-magic-numbers
expect(targetService.echo).toHaveBeenCalledTimes(2);
}));
});
20 changes: 10 additions & 10 deletions lib/mock-helper/mock-helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ export class ExampleStructuralDirective {
selector: 'component-a',
template: 'body-a',
})
export class AComponent {
}
export class AComponent {}

@Component({
selector: 'component-b',
template: 'body-b',
})
export class BComponent {
}
export class BComponent {}

describe('MockHelper:getDirective', () => {
beforeEach(async(() => {
Expand Down Expand Up @@ -131,17 +129,19 @@ describe('MockHelper:getDirective', () => {
const componentA = MockHelper.findOrFail(fixture.debugElement, AComponent);
expect(componentA.componentInstance).toEqual(jasmine.any(AComponent));

expect(() => MockHelper.findOrFail(componentA, BComponent))
.toThrowError('Cannot find an element via MockHelper.findOrFail');
expect(() => MockHelper.findOrFail(componentA, BComponent)).toThrowError(
'Cannot find an element via MockHelper.findOrFail'
);
});

it('find selector: string', () => {
const fixture = MockRender(`<component-b></component-b>`);
const componentB = MockHelper.findOrFail(fixture.debugElement, 'component-b');
expect(componentB.componentInstance).toEqual(jasmine.any(BComponent));

expect(() => MockHelper.findOrFail(componentB, AComponent))
.toThrowError('Cannot find an element via MockHelper.findOrFail');
expect(() => MockHelper.findOrFail(componentB, AComponent)).toThrowError(
'Cannot find an element via MockHelper.findOrFail'
);
});

it('find selector: T', () => {
Expand All @@ -165,7 +165,7 @@ describe('MockHelper:getDirective', () => {
it('findAll selector: T', () => {
const fixture = MockRender(`<component-a></component-a><component-a></component-a>`);
const componentA = MockHelper.findAll(fixture.debugElement, AComponent);
expect(componentA.length).toBe(2); // tslint:disable-line:no-magic-numbers
expect(componentA.length).toBe(2);
expect(componentA[0].componentInstance).toEqual(jasmine.any(AComponent));
expect(componentA[1].componentInstance).toEqual(jasmine.any(AComponent));

Expand All @@ -176,7 +176,7 @@ describe('MockHelper:getDirective', () => {
it('findAll selector: string', () => {
const fixture = MockRender(`<component-b></component-b><component-b></component-b>`);
const componentB = MockHelper.findAll(fixture.debugElement, 'component-b');
expect(componentB.length).toEqual(2); // tslint:disable-line:no-magic-numbers
expect(componentB.length).toEqual(2);
expect(componentB[0].componentInstance).toEqual(jasmine.any(BComponent));
expect(componentB[0].componentInstance).toEqual(jasmine.any(BComponent));

Expand Down
100 changes: 97 additions & 3 deletions lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* tslint:disable:variable-name unified-signatures */

import { Type } from '@angular/core';
import { EventEmitter, Type } from '@angular/core';
import { By } from '@angular/platform-browser';

import { directiveResolver } from '../common/reflect';
import { MockedDebugElement, MockedDebugNode } from '../mock-render';
import { MockedFunction, mockServiceHelper } from '../mock-service';

Expand All @@ -29,13 +30,106 @@ export const MockHelper: {
findOrFail<T = any>(debugElement: MockedDebugElement, cssSelector: string): MockedDebugElement<T>;
getDirective<T>(debugNode: MockedDebugNode, directive: Type<T>): undefined | T;
getDirectiveOrFail<T>(debugNode: MockedDebugNode, directive: Type<T>): T;
getInput<T = any>(debugNode: MockedDebugNode, input: string): undefined | T;
getInputOrFail<T = any>(debugNode: MockedDebugNode, input: string): T;
getOutput<T = any>(debugNode: MockedDebugNode, output: string): undefined | EventEmitter<T>;
getOutputOrFail<T = any>(debugNode: MockedDebugNode, output: string): EventEmitter<T>;
mockService<I extends object, O extends object>(instance: I, overrides: O): I & O;
mockService<T = MockedFunction>(instance: any, name: string, style?: 'get' | 'set'): T;
} = {
getInput: (debugNode: MockedDebugNode, input: string): any => {
for (const token of debugNode.providerTokens) {
const { inputs } = directiveResolver.resolve(token);
if (!inputs) {
continue;
}
for (const inputDef of inputs) {
const [prop = '', alias = ''] = inputDef.split(':', 2).map(v => v.trim());
if (!prop) {
continue;
}
if (!alias && prop !== input) {
continue;
}
if (alias !== input) {
continue;
}
const directive: any = MockHelper.getDirective(debugNode, token);
if (!directive) {
continue;
}
return directive[prop];
}
}
},

getInputOrFail: (debugNode: MockedDebugNode, input: string): any => {
// for inputs with a value of undefined it's hard to detect if it exists or doesn't.
// therefore we have copy-paste until best times when someone combines them correctly together.
for (const token of debugNode.providerTokens) {
const { inputs } = directiveResolver.resolve(token);
if (!inputs) {
continue;
}
for (const inputDef of inputs) {
const [prop = '', alias = ''] = inputDef.split(':', 2).map(v => v.trim());
if (!prop) {
continue;
}
if (!alias && prop !== input) {
continue;
}
if (alias !== input) {
continue;
}
const directive: any = MockHelper.getDirective(debugNode, token);
if (!directive) {
continue;
}
return directive[prop];
}
}
throw new Error(`Cannot find ${input} input via MockHelper.getInputOrFail`);
},

getOutput: (debugNode: MockedDebugNode, output: string): any => {
for (const token of debugNode.providerTokens) {
const { outputs } = directiveResolver.resolve(token);
if (!outputs) {
continue;
}
for (const outputDef of outputs) {
const [prop = '', alias = ''] = outputDef.split(':', 2).map(v => v.trim());
if (!prop) {
continue;
}
if (!alias && prop !== output) {
continue;
}
if (alias !== output) {
continue;
}
const directive: any = MockHelper.getDirective(debugNode, token);
if (!directive) {
continue;
}
return directive[prop];
}
}
},

getOutputOrFail: (debugNode: MockedDebugNode, output: string): any => {
const result = MockHelper.getOutput(debugNode, output);
if (!result) {
throw new Error(`Cannot find ${output} output via MockHelper.getOutputOrFail`);
}
return result;
},

getDirectiveOrFail: <T>(debugNode: MockedDebugNode, directive: Type<T>): T => {
const result = MockHelper.getDirective(debugNode, directive);
if (!result) {
throw new Error(`Cannot find a directive via MockHelper.getDirectiveOrFail`);
throw new Error(`Cannot find ${directive.name} directive via MockHelper.getDirectiveOrFail`);
}
return result;
},
Expand Down Expand Up @@ -72,7 +166,7 @@ export const MockHelper: {
findDirectiveOrFail: <T>(debugNode: MockedDebugNode, directive: Type<T>): T => {
const result = MockHelper.findDirective(debugNode, directive);
if (!result) {
throw new Error(`Cannot find a directive via MockHelper.findDirectiveOrFail`);
throw new Error(`Cannot find ${directive.name} directive via MockHelper.findDirectiveOrFail`);
}
return result;
},
Expand Down
6 changes: 4 additions & 2 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
"rules": {
"align": false,
"arrow-parens": false,
"completed-docs": false,
"comment-format": false,
"completed-docs": false,
"file-name-casing": false,
"max-classes-per-file": false,
"member-access": false,
"newline-before-return": false,
"newline-per-chained-call": false,
"no-any": false,
"no-disabled-tests": true,
"no-empty": false,
"no-implicit-dependencies": [true, "dev", ["ng-mocks"]],
"no-floating-promises": false,
"no-focused-tests": true,
"no-implicit-dependencies": [true, "dev", ["ng-mocks"]],
"no-magic-numbers": false,
"no-submodule-imports": false,
"no-unbound-method": false,
"no-unsafe-any": false,
Expand Down

0 comments on commit af9a846

Please sign in to comment.