Skip to content

Commit

Permalink
fix(core): ignoring host bindings in mocks #1427
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Jan 18, 2022
1 parent c01d591 commit 411842c
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 47 deletions.
2 changes: 1 addition & 1 deletion karma.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default (config: Config) => {
reporters: ['dots', ...(process.env.WITH_COVERAGE === undefined ? [] : ['junit', 'coverage-istanbul'])],
singleRun: true,
webpack: {
devtool: 'inline-source-map',
devtool: 'eval-source-map',
module: {
rules: [
...(process.env.WITH_COVERAGE === undefined
Expand Down
12 changes: 7 additions & 5 deletions libs/ng-mocks/src/lib/mock/decorate-declaration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tslint:disable cyclomatic-complexity

import { Component, Directive, HostBinding, HostListener, Provider, ViewChild } from '@angular/core';
import { Component, Directive, Provider, ViewChild } from '@angular/core';

import { AnyType } from '../common/core.types';
import decorateInputs from '../common/decorate.inputs';
Expand Down Expand Up @@ -65,16 +65,18 @@ export default <T extends Component | Directive>(
config.queryScanKeys = decorateQueries(mock, meta.queries);

config.hostBindings = [];
for (const [key, ...args] of meta.hostBindings || []) {
HostBinding(...args)(mock.prototype, key);
for (const [key] of meta.hostBindings || []) {
// mock declarations should not have side effects based on host bindings.
// HostBinding(...args)(mock.prototype, key);
if (config.hostBindings.indexOf(key) === -1) {
config.hostBindings.push(key);
}
}

config.hostListeners = [];
for (const [key, ...args] of meta.hostListeners || []) {
HostListener(...args)(mock.prototype, key);
for (const [key] of meta.hostListeners || []) {
// mock declarations should not have side effects based on host bindings.
// HostListener(...args)(mock.prototype, key);
if (config.hostListeners.indexOf(key) === -1) {
config.hostListeners.push(key);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export class DivCls {
selector: 'base2',
})
export class BaseCls {
@ContentChild(DivCls) public contentChildBase?: DivCls;
@ContentChildren(DivCls) public contentChildrenBase?: QueryList<DivCls>;
@ContentChild(DivCls, {} as any) public contentChildBase?: DivCls;
@ContentChildren(DivCls, {} as any) public contentChildrenBase?: QueryList<DivCls>;

@HostBinding('attr.base1') public hostBase1: any;
@HostBinding('attr.base2') public hostBase2: any;
Expand All @@ -75,9 +75,6 @@ export class BaseCls {
@Input() public propBase1: EventEmitter<void> | string = '';
@Output() public propBase2 = new EventEmitter<void>();

@ViewChild(DivCls) public viewChildBase?: DivCls;
@ViewChildren(DivCls) public viewChildrenBase?: QueryList<DivCls>;

@HostListener('focus') public hostBaseHandler3() {
this.hostBase3 = 'base3';
}
Expand All @@ -92,8 +89,8 @@ export class BaseCls {
template: `override2<ng-content></ng-content>`,
})
export class OverrideCls extends BaseCls {
@ContentChild(DivCls) public contentChildOverride?: DivCls;
@ContentChildren(DivCls) public contentChildrenOverride?: QueryList<DivCls>;
@ContentChild(DivCls, {} as any) public contentChildOverride?: DivCls;
@ContentChildren(DivCls, {} as any) public contentChildrenOverride?: QueryList<DivCls>;

@HostBinding('attr.override2') public hostBase2: any;
@HostBinding('attr.override1') public hostOverride1: any;
Expand All @@ -107,9 +104,6 @@ export class OverrideCls extends BaseCls {

@Output() public propOverride2 = new EventEmitter<void>();

@ContentChild(DivCls) public viewChildBase?: DivCls;
@ContentChildren(DivCls) public viewChildrenBase?: QueryList<DivCls>;

@HostListener('click') public hostBaseHandler3() {
this.hostOverride3 = 'override3';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { EventEmitter, HostBinding } from '@angular/core';
// tslint:disable cyclomatic-complexity

import { EventEmitter } from '@angular/core';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

import {
Expand Down Expand Up @@ -141,10 +143,18 @@ describe('double-declarations', () => {

// renders component
const html = ngMocks.formatHtml(fixture);
expect(html).toContain('base1="base1"');
expect(html).toContain('base2="override2"');
expect(html).toContain('override2="override2"');
expect(html).toContain('override1="override1"');
if (contextName === 'real') {
expect(html).toContain('base1="base1"');
expect(html).toContain('base2="override2"');
expect(html).toContain('override2="override2"');
expect(html).toContain('override1="override1"');
} else {
// but doesn't not render host bindings in mock declarations.
expect(html).not.toContain('base1="base1"');
expect(html).not.toContain('base2="override2"');
expect(html).not.toContain('override2="override2"');
expect(html).not.toContain('override1="override1"');
}
});

it('fails on override2', () => {
Expand Down Expand Up @@ -194,9 +204,7 @@ describe('double-declarations', () => {
MockRender(
`<override1
[prop1]="'prop1'"
[prop2alias]="'prop2alias'"
[override2alias]="'override2alias'"
[prop3alias]="'prop3alias'"
[override3alias]="'override3alias'"
[propBase1]="'propBase1'"
[propOverride1]="'propOverride1'"
Expand Down Expand Up @@ -224,9 +232,9 @@ describe('double-declarations', () => {
const instance = ngMocks.findInstance(OverrideCls);
(instance.prop1 as EventEmitter<void>).emit();
expect(data.value).toEqual('prop1');
(instance.propBase2 as EventEmitter<void>).emit();
instance.propBase2.emit();
expect(data.value).toEqual('propBase2');
(instance.propOverride2 as EventEmitter<void>).emit();
instance.propOverride2.emit();
expect(data.value).toEqual('propOverride2');
});

Expand All @@ -241,9 +249,10 @@ describe('double-declarations', () => {
});
expect(triggers).toEqual(0);
ngMocks.trigger(instanceEl, 'focus');
expect(triggers).toEqual(1);
// host listeners are not triggered in mock declarations
expect(triggers).toEqual(contextName === 'real' ? 1 : 0);
ngMocks.trigger(instanceEl, 'click');
expect(triggers).toEqual(2);
expect(triggers).toEqual(contextName === 'real' ? 2 : 0);
});

it('respects content injections', () => {
Expand All @@ -253,36 +262,60 @@ describe('double-declarations', () => {
);
const instance = ngMocks.findInstance(OverrideCls);

expect(instance.contentChildBase?.prop).toEqual(1);
expect(instance.contentChildrenBase?.first.prop).toEqual(1);
expect(instance.contentChildrenBase?.length).toEqual(1);

expect(instance.contentChildOverride?.prop).toEqual(1);
expect(
instance.contentChildrenOverride?.first.prop,
instance.contentChildBase &&
instance.contentChildBase.prop,
).toEqual(1);
expect(
instance.contentChildrenBase &&
instance.contentChildrenBase.first.prop,
).toEqual(1);
expect(
instance.contentChildrenBase &&
instance.contentChildrenBase.length,
).toEqual(1);
expect(instance.contentChildrenOverride?.length).toEqual(1);

// looks like parent views wins
expect(instance.viewChildBase).toBeUndefined();
expect(instance.viewChildrenBase?.length).toEqual(0);
expect(
instance.contentChildOverride &&
instance.contentChildOverride.prop,
).toEqual(1);
expect(
instance.contentChildrenOverride &&
instance.contentChildrenOverride.first.prop,
).toEqual(1);
expect(
instance.contentChildrenOverride &&
instance.contentChildrenOverride.length,
).toEqual(1);

fixture.componentInstance.value = 2;
fixture.detectChanges();

expect(instance.contentChildBase?.prop).toEqual(2);
expect(instance.contentChildrenBase?.first.prop).toEqual(2);
expect(instance.contentChildrenBase?.length).toEqual(1);

expect(instance.contentChildOverride?.prop).toEqual(2);
expect(
instance.contentChildrenOverride?.first.prop,
instance.contentChildBase &&
instance.contentChildBase.prop,
).toEqual(2);
expect(
instance.contentChildrenBase &&
instance.contentChildrenBase.first.prop,
).toEqual(2);
expect(instance.contentChildrenOverride?.length).toEqual(1);
expect(
instance.contentChildrenBase &&
instance.contentChildrenBase.length,
).toEqual(1);

// looks like parent views wins
expect(instance.viewChildBase).toBeUndefined();
expect(instance.viewChildrenBase?.length).toEqual(0);
expect(
instance.contentChildOverride &&
instance.contentChildOverride.prop,
).toEqual(2);
expect(
instance.contentChildrenOverride &&
instance.contentChildrenOverride.first.prop,
).toEqual(2);
expect(
instance.contentChildrenOverride &&
instance.contentChildrenOverride.length,
).toEqual(1);
});
});
});
Expand Down
31 changes: 31 additions & 0 deletions tests/issue-1427/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Component, HostBinding, HostListener } from '@angular/core';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

@Component({
selector: 'target',
template: '{{ id }}',
})
export class TargetComponent {
@HostBinding() public id = `custom-form`;
@HostListener('click') public click = () => undefined;
}

describe('issue-1427', () => {
beforeEach(() => MockBuilder(null, TargetComponent));

it('ignores host bindings in mock declarations', () => {
const fixture = MockRender(TargetComponent);

// HostBinding with id doesn't cause a side effect.
expect(ngMocks.formatHtml(fixture)).toEqual('<target></target>');

// HostListener doesn't cause a side effect.
expect(
fixture.point.componentInstance.click,
).not.toHaveBeenCalled();
ngMocks.trigger(fixture.point, 'click');
expect(
fixture.point.componentInstance.click,
).not.toHaveBeenCalled();
});
});

0 comments on commit 411842c

Please sign in to comment.