Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mock-render tries to mirror passed component #138

Merged
merged 1 commit into from
Jun 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/mock-helper/mock-helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,19 @@ describe('MockHelper:getDirective', () => {
const componentA = ngMocks.find(fixture.debugElement, AComponent);
expect(componentA.componentInstance).toEqual(jasmine.any(AComponent));

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

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

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

it('find selector: T', () => {
Expand Down
4 changes: 2 additions & 2 deletions lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const ngMocks: {
} = {
find: (...args: any[]) => {
const el: MockedDebugElement = args[0];
const sel: any = args[1];
const sel: string | Type<any> = args[1];
const notFoundValue: any = args.length === 3 ? args[2] : defaultNotFoundValue;

const term = typeof sel === 'string' ? By.css(sel) : By.directive(getSourceOfMock(sel));
Expand All @@ -136,7 +136,7 @@ export const ngMocks: {
return notFoundValue;
}
if (!result) {
throw new Error(`Cannot find an element via ngMocks.find`);
throw new Error(`Cannot find an element via ngMocks.find(${typeof sel === 'string' ? sel : sel.name})`);
}
},

Expand Down
35 changes: 31 additions & 4 deletions lib/mock-render/mock-render.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import createSpy = jasmine.createSpy;
import { DOCUMENT } from '@angular/common';
import { DebugElement, DebugNode, EventEmitter } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { first } from 'rxjs/operators';

import { ngMocks } from '../mock-helper';
import { MockService } from '../mock-service';

import { MockedComponentFixture, MockedDebugElement, MockedDebugNode, MockRender } from './mock-render';
import { RenderRealComponent, WithoutSelectorComponent } from './mock-render.fixtures';
import { DebugElement, DebugNode } from '@angular/core';

describe('MockRender', () => {
beforeEach(() => {
Expand All @@ -17,7 +18,7 @@ describe('MockRender', () => {
});

it('renders any template and respects dynamic params', () => {
const spy = createSpy('mockClick');
const spy = jasmine.createSpy('mockClick');
const assertPayload = {
magic: Math.random(),
};
Expand Down Expand Up @@ -69,7 +70,7 @@ describe('MockRender', () => {
});

it('binds inputs and outputs with a provided component', () => {
const spy = createSpy('click');
const spy = jasmine.createSpy('click');
const fixture = MockRender(RenderRealComponent, {
click: spy,
content: 'content',
Expand Down Expand Up @@ -119,6 +120,32 @@ describe('MockRender', () => {
expect(fixture.debugElement.nativeElement.innerHTML).toEqual('');
});

it('assigns outputs to a literals', () => {
const fixture = MockRender(RenderRealComponent, {
click: undefined,
});

const expected = {
value: Math.random(),
};
ngMocks.find(fixture.debugElement, 'span').triggerEventHandler('click', expected);
expect(fixture.componentInstance.click as any).toEqual(expected);
});

it('assigns outputs to an EventEmitter', () => {
const fixture = MockRender(RenderRealComponent, {
click: new EventEmitter(),
});

const expected = {
value: Math.random(),
};
let actual: any;
fixture.componentInstance.click.pipe(first()).subscribe(value => (actual = value));
ngMocks.find(fixture.debugElement, 'span').triggerEventHandler('click', expected);
expect(actual).toEqual(expected);
});

it('assigns DebugNodes and DebugElements to Mocks and back', () => {
const debugNode = ({} as any) as DebugNode;
const debugElement = ({} as any) as DebugElement;
Expand Down
93 changes: 87 additions & 6 deletions lib/mock-render/mock-render.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { core } from '@angular/compiler';
import { Component, DebugElement, DebugNode, Provider } from '@angular/core';
import { Component, DebugElement, DebugNode, EventEmitter, Provider } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Subject } from 'rxjs';

import { Type } from '../common';
import { directiveResolver } from '../common/reflect';
import { ngMocks } from '../mock-helper';
import { mockServiceHelper } from '../mock-service';

// tslint:disable-next-line:interface-name
export interface MockedDebugNode<T = any> extends DebugNode {
Expand All @@ -26,14 +28,35 @@ export interface MockedComponentFixture<C = any, F = undefined> extends Componen
point: MockedDebugElement<C>;
}

// tslint:disable: interface-over-type-literal interface-name
export type DefaultRenderComponent<MComponent extends Record<keyof any, any>> = {
[K in keyof MComponent]: MComponent[K];
};
// tslint:enable: interface-over-type-literal interface-name

function solveOutput(output: any): string {
if (typeof output === 'function') {
return '($event)';
}
if (output && typeof output === 'object' && output instanceof EventEmitter) {
return '.emit($event)';
}
if (output && typeof output === 'object' && output instanceof Subject) {
return '.next($event)';
}
return '=$event';
}

function MockRender<MComponent, TComponent extends { [key: string]: any }>(
template: Type<MComponent>,
params: TComponent,
detectChanges?: boolean | IMockRenderOptions
): MockedComponentFixture<MComponent, TComponent>;

// without params we shouldn't autocomplete any keys of any types.
function MockRender<MComponent>(template: Type<MComponent>): MockedComponentFixture<MComponent>;
function MockRender<MComponent extends Record<keyof any, any>>(
template: Type<MComponent>
): MockedComponentFixture<MComponent, DefaultRenderComponent<MComponent>>;

function MockRender<MComponent = any, TComponent extends { [key: string]: any } = { [key: string]: any }>(
template: string,
Expand All @@ -50,7 +73,12 @@ function MockRender<MComponent, TComponent extends { [key: string]: any }>(
flags: boolean | IMockRenderOptions = true
): MockedComponentFixture<MComponent, TComponent> {
const flagsObject: IMockRenderOptions = typeof flags === 'boolean' ? { detectChanges: flags } : flags;
const isComponent = typeof template !== 'string';
const noParams = !params;

let inputs: string[] | undefined = [];
let outputs: string[] | undefined = [];
let selector: string | undefined = '';
let mockedTemplate = '';
if (typeof template === 'string') {
mockedTemplate = template;
Expand All @@ -64,7 +92,10 @@ function MockRender<MComponent, TComponent extends { [key: string]: any }>(
}
}

const { inputs, outputs, selector } = meta;
inputs = meta.inputs;
outputs = meta.outputs;
selector = meta.selector;

mockedTemplate += selector ? `<${selector}` : '';
if (selector && inputs) {
inputs.forEach((definition: string) => {
Expand All @@ -73,16 +104,24 @@ function MockRender<MComponent, TComponent extends { [key: string]: any }>(
mockedTemplate += ` [${alias}]="${alias}"`;
} else if (property && params && typeof params[property]) {
mockedTemplate += ` [${property}]="${property}"`;
} else if (alias && noParams) {
mockedTemplate += ` [${alias}]="${property}"`;
} else if (noParams) {
mockedTemplate += ` [${property}]="${property}"`;
}
});
}
if (selector && outputs) {
outputs.forEach((definition: string) => {
const [property, alias] = definition.split(': ');
if (alias && params && typeof params[alias]) {
mockedTemplate += ` (${alias})="${alias}($event)"`;
mockedTemplate += ` (${alias})="${alias}${solveOutput(params[alias])}"`;
} else if (property && params && typeof params[property]) {
mockedTemplate += ` (${property})="${property}($event)"`;
mockedTemplate += ` (${property})="${property}${solveOutput(params[property])}"`;
} else if (alias && noParams) {
mockedTemplate += ` (${alias})="${property}.emit($event)"`;
} else if (noParams) {
mockedTemplate += ` (${property})="${property}.emit($event)"`;
}
});
}
Expand All @@ -97,7 +136,19 @@ function MockRender<MComponent, TComponent extends { [key: string]: any }>(
const component = Component(options)(
class MockRenderComponent {
constructor() {
Object.assign(this, params);
Object.assign(this as any, params);
if (noParams && isComponent && inputs) {
for (const definition of inputs) {
const [property] = definition.split(': ');
(this as any)[property] = undefined;
}
}
if (noParams && isComponent && outputs) {
for (const definition of outputs) {
const [property] = definition.split(': ');
(this as any)[property] = new EventEmitter();
}
}
}
} as Type<TComponent>
);
Expand All @@ -117,6 +168,36 @@ function MockRender<MComponent, TComponent extends { [key: string]: any }>(
}

fixture.point = fixture.debugElement.children[0];
if (noParams && typeof template === 'function') {
const properties = mockServiceHelper.extractPropertiesFromPrototype(template.prototype);
const exists = Object.getOwnPropertyNames(fixture.componentInstance);
for (const property of properties) {
if (exists.indexOf(property) !== -1) {
continue;
}
Object.defineProperty(fixture.componentInstance, property, {
get: () => fixture.point.componentInstance[property],
set: (v: any) => (fixture.point.componentInstance[property] = v),

configurable: true,
enumerable: true,
});
}
const methods = mockServiceHelper.extractMethodsFromPrototype(template.prototype);
for (const method of methods) {
if (exists.indexOf(method) !== -1) {
continue;
}
Object.defineProperty(fixture.componentInstance, method, {
value: (...args: any[]) => fixture.point.componentInstance[method](...args),

configurable: true,
enumerable: true,
writable: true,
});
}
}

return fixture;
}

Expand Down
83 changes: 83 additions & 0 deletions tests/mock-render-mirrors-component/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Component, EventEmitter, Input, NgModule, Output } from '@angular/core';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
import { first } from 'rxjs/operators';

@Component({
selector: 'target',
template: `
<div data-role="input1">{{ input1 || 'input1' }}</div>
<div data-role="input2">{{ input2 || 'input2' }}</div>
<div data-role="output1" (click)="output1.emit()">output1</div>
<div data-role="output2" (click)="output2.emit()">output2</div>
<div data-role="var1">{{ var1 || 'var1' }}</div>
<div data-role="var2">{{ var2 || 'var2' }}</div>
`,
})
export class TargetComponent {
@Input() public input1: string | null = null;
@Input('input') public input2: string | null = null;

@Output() public output1: EventEmitter<void> = new EventEmitter();
@Output('output') public output2: EventEmitter<void> = new EventEmitter();

public var1 = '';
public var2 = '';

public test(var2: string): void {
this.var2 = var2;
}
}

@NgModule({
declarations: [TargetComponent],
})
export class TargetModule {}

describe('mock-render-mirrors-component', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule));

it('mirrors the desired component if no params were passed', () => {
const fixture = MockRender(TargetComponent);

const input1 = ngMocks.find(fixture.debugElement, '[data-role="input1"]');
const input2 = ngMocks.find(fixture.debugElement, '[data-role="input2"]');
const output1 = ngMocks.find(fixture.debugElement, '[data-role="output1"]');
const output2 = ngMocks.find(fixture.debugElement, '[data-role="output2"]');
const var1 = ngMocks.find(fixture.debugElement, '[data-role="var1"]');
const var2 = ngMocks.find(fixture.debugElement, '[data-role="var2"]');

// initial state
expect(input1.nativeElement.innerHTML).toEqual('input1');
expect(input2.nativeElement.innerHTML).toEqual('input2');
expect(output1.nativeElement.innerHTML).toEqual('output1');
expect(output2.nativeElement.innerHTML).toEqual('output2');
expect(var1.nativeElement.innerHTML).toEqual('var1');
expect(var2.nativeElement.innerHTML).toEqual('var2');

// updating inputs and properties, calling methods.
fixture.componentInstance.input1 = 'updatedInput1';
fixture.componentInstance.input2 = 'updatedInput2';
fixture.componentInstance.var1 = 'updatedVar1';
fixture.componentInstance.test('updatedVar2');
fixture.detectChanges();

// checking that the data has been proxied correctly
expect(input1.nativeElement.innerHTML).toEqual('updatedInput1');
expect(input2.nativeElement.innerHTML).toEqual('updatedInput2');
// doesn't work because we can't correctly detect it via defineProperty.
// expect(var1.nativeElement.innerHTML).toEqual('updatedVar1');
expect(var2.nativeElement.innerHTML).toEqual('updatedVar2');

// checking output1
let updatedOutput1 = false;
fixture.componentInstance.output1.pipe(first()).subscribe(() => (updatedOutput1 = true));
output1.triggerEventHandler('click', null);
expect(updatedOutput1).toBe(true);

// checking output2
let updatedOutput2 = false;
fixture.componentInstance.output2.pipe(first()).subscribe(() => (updatedOutput2 = true));
output2.triggerEventHandler('click', null);
expect(updatedOutput2).toBe(true);
});
});