From af9a846c9389b84c01a8366bbd28de90d038e9f5 Mon Sep 17 00:00:00 2001 From: MG Date: Sat, 23 May 2020 12:17:38 +0200 Subject: [PATCH] feat: ease of getting inputs and outputs MockHelper.getInput MockHelper.getInputOrFail MockHelper.getOutput MockHelper.getOutputOrFail closes #129 --- README.md | 22 +++++- e2e/get-inputs-and-outputs/fixtures.ts | 33 ++++++++ e2e/get-inputs-and-outputs/test.spec.ts | 74 ++++++++++++++++++ e2e/spies/test.spec.ts | 4 +- lib/mock-helper/mock-helper.spec.ts | 20 ++--- lib/mock-helper/mock-helper.ts | 100 +++++++++++++++++++++++- tslint.json | 6 +- 7 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 e2e/get-inputs-and-outputs/fixtures.ts create mode 100644 e2e/get-inputs-and-outputs/test.spec.ts diff --git a/README.md b/README.md index 92e4b2ad68..fa4d3a26bd 100644 --- a/README.md +++ b/README.md @@ -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 @@ -489,6 +494,15 @@ const component: MockedDebugElement = MockHelper.findOrFail(fixture.d const component: MockedDebugElement = 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 | undefined = MockHelper.getOutput(debugElement, 'update'); +const outputValue: EventEmitter = MockHelper.getOutputOrFail(debugElement, 'update'); +``` + In case if we want to mock methods / properties of a service / provider. ```typescript @@ -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 diff --git a/e2e/get-inputs-and-outputs/fixtures.ts b/e2e/get-inputs-and-outputs/fixtures.ts new file mode 100644 index 0000000000..88000035aa --- /dev/null +++ b/e2e/get-inputs-and-outputs/fixtures.ts @@ -0,0 +1,33 @@ +import { Component, Directive, EventEmitter, Input, NgModule, Output } from '@angular/core'; + +@Component({ + selector: 'target', + template: '{{ input }}', +}) +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 {} diff --git a/e2e/get-inputs-and-outputs/test.spec.ts b/e2e/get-inputs-and-outputs/test.spec.ts new file mode 100644 index 0000000000..48d712f5fe --- /dev/null +++ b/e2e/get-inputs-and-outputs/test.spec.ts @@ -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( + ``, + 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(); + }); +}); diff --git a/e2e/spies/test.spec.ts b/e2e/spies/test.spec.ts index 43ecb97cd1..9d9a2464e5 100644 --- a/e2e/spies/test.spec.ts +++ b/e2e/spies/test.spec.ts @@ -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); })); }); @@ -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); })); }); diff --git a/lib/mock-helper/mock-helper.spec.ts b/lib/mock-helper/mock-helper.spec.ts index ff3261a3a7..cfa1746028 100644 --- a/lib/mock-helper/mock-helper.spec.ts +++ b/lib/mock-helper/mock-helper.spec.ts @@ -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(() => { @@ -131,8 +129,9 @@ 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', () => { @@ -140,8 +139,9 @@ describe('MockHelper:getDirective', () => { 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', () => { @@ -165,7 +165,7 @@ describe('MockHelper:getDirective', () => { it('findAll selector: T', () => { const fixture = MockRender(``); 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)); @@ -176,7 +176,7 @@ describe('MockHelper:getDirective', () => { it('findAll selector: string', () => { const fixture = MockRender(``); 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)); diff --git a/lib/mock-helper/mock-helper.ts b/lib/mock-helper/mock-helper.ts index 89b13550de..fa3a63ed38 100644 --- a/lib/mock-helper/mock-helper.ts +++ b/lib/mock-helper/mock-helper.ts @@ -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'; @@ -29,13 +30,106 @@ export const MockHelper: { findOrFail(debugElement: MockedDebugElement, cssSelector: string): MockedDebugElement; getDirective(debugNode: MockedDebugNode, directive: Type): undefined | T; getDirectiveOrFail(debugNode: MockedDebugNode, directive: Type): T; + getInput(debugNode: MockedDebugNode, input: string): undefined | T; + getInputOrFail(debugNode: MockedDebugNode, input: string): T; + getOutput(debugNode: MockedDebugNode, output: string): undefined | EventEmitter; + getOutputOrFail(debugNode: MockedDebugNode, output: string): EventEmitter; mockService(instance: I, overrides: O): I & O; mockService(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: (debugNode: MockedDebugNode, directive: Type): 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; }, @@ -72,7 +166,7 @@ export const MockHelper: { findDirectiveOrFail: (debugNode: MockedDebugNode, directive: Type): 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; }, diff --git a/tslint.json b/tslint.json index de4558bfaf..6a56144bf4 100644 --- a/tslint.json +++ b/tslint.json @@ -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,