Skip to content

Commit

Permalink
feat(core): support of HostDirectives #5117
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Apr 2, 2023
1 parent 9ccd352 commit b8414a4
Show file tree
Hide file tree
Showing 17 changed files with 1,127 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ overrides:
- 500
max-lines-per-function:
- error
- 100
- 150

'@angular-eslint/no-input-rename': off
'@angular-eslint/no-output-rename': off
'@angular-eslint/no-outputs-metadata-property': off

'@typescript-eslint/no-explicit-any': off
'@typescript-eslint/no-namespace': off
Expand Down
115 changes: 115 additions & 0 deletions docs/articles/guides/host-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: How to test a host directive in Angular application
description: Covering an Angular host directive with tests
sidebar_label: Host Directive
---

Let's imagine we have a component with a host directive which adds the `name` attribute.

The code of the directive:

```ts
@Directive({
selector: 'host',
standalone: true,
})
class HostDirective {
@HostBinding('attr.name') @Input() input?: string;
}
```

The code of the component:

```ts
@Component({
selector: 'target',
hostDirectives: [
{
directive: HostDirective,
inputs: ['input'],
},
],
template: 'target',
})
class TargetComponent {
// tons of logic we want to ignore
}
```

The component can be heavy, and, in an ideal test, the logic of the component should be ignored,
so the focus would stay on the directive and how it behaves.

[`MockBuilder`](../api/MockBuilder.md) knows how to mock the component
and how to keep one or some of its host directives as they are.

In order to do so, the host directive should be kept, and its component should be mocked:

```ts
beforeEach(() => MockBuilder(HostDirective, TargetComponent));
```

Profit!

To access the directive in a test, [`ngMocks.findInstnace`](../api/ngMocks/findInstance.md) can be used.

```ts
it('keeps host directives', () => {
const fixture = MockRender(TargetComponent, { input: 'test' });

const directive = ngMocks.findInstance(HostDirective);
expect(directive.input).toEqual('test');
expect(ngMocks.formatHtml(fixture)).toContain(' name="test"');
});
```

## Live example

- [Try it on CodeSandbox](https://codesandbox.io/s/github/help-me-mom/ng-mocks-sandbox/tree/tests?file=/src/examples/TestHostDirective/test.spec.ts&initialpath=%3Fspec%3DTestHostDirective)
- [Try it on StackBlitz](https://stackblitz.com/github/help-me-mom/ng-mocks-sandbox/tree/tests?file=src/examples/TestHostDirective/test.spec.ts&initialpath=%3Fspec%3DTestHostDirective)

```ts title="https://github.com/help-me-mom/ng-mocks/blob/master/examples/TestHostDirective/test.spec.ts"
import {
Component,
Directive,
HostBinding,
Input,
} from '@angular/core';

import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

@Directive({
selector: 'host',
standalone: true,
})
class HostDirective {
@HostBinding('attr.name') @Input() input?: string;

public hostTestHostDirective() {}
}

@Component({
selector: 'target',
hostDirectives: [
{
directive: HostDirective,
inputs: ['input'],
},
],
template: 'target',
})
class TargetComponent {
public targetTestHostDirective() {}
}

describe('TestHostDirective', () => {
beforeEach(() => MockBuilder(HostDirective, TargetComponent));

it('keeps host directives', () => {
const fixture = MockRender(TargetComponent, { input: 'test' });

const directive = ngMocks.findInstance(HostDirective);
expect(directive.input).toEqual('test');
expect(ngMocks.formatHtml(fixture)).toContain(' name="test"');
});
});
```
149 changes: 149 additions & 0 deletions docs/articles/guides/mock/host-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: How to mock a host directive
description: Mocking an Angular host directive
sidebar_label: Host Directive
---

It can happen that a component hast a host directive which should be mocked in a test.

There are several ways how `ng-mocks` can mock host directives:

- [`MockBuilder`](../../api/MockBuilder.md#shallow-flag) and its [`shallow`](../../api/MockBuilder.md#shallow-flag) flag
- [`MockBuilder`](../../api/MockBuilder.md) constructor
- `TestBed`

## `shallow` flag

It's the easiest and recommended way which covers all host directives automatically, so there is no need to specify all of them.

To mock all host directives, simply provide [`shallow`](../../api/MockBuilder.md#shallow-flag) flag in [`MockBuilder.mock`](../../api/MockBuilder.md#mock):

```ts
beforeEach(() =>
MockBuilder().mock(TargetComponent, { shallow: true }),
);
```

Now, all host directives and their dependencies will be mocks.

## `MockBuilder`

[`MockBuilder`](../../api/MockBuilder.md) is useful, when only one or some host directives should be mocked.

To do so, the host directives should be specified as the second parameter of [`MockBuilder`](../../api/MockBuilder.md):

```ts
beforeEach(() => MockBuilder(TargetComponent, HostDirective));
```

That's it, now `TargetComponent` will have a mock of `HostDirective`.

## TestBed

If you use `TestBed`, you should mock the desired host directive with [`MockDirective`](../../api/MockDirective.md)
and import / declare its component.

For example, if the name of the component is `TargetComponent` and its host directive is called `HostDirective`,
then `TestBed` can be defined like that:

```ts
beforeEach(() =>
TestBed.configureTestingModule({
imports: [MockDirective(HostDirective)], // mocking the host directive
declarations: [TargetComponent], // declaring the component under test
}).compileComponents(),
);
```

Profit! Under the hood `TargetComponent` will be redefined to use a mock of `HostDirective`.

## Live example

- [Try it on CodeSandbox](https://codesandbox.io/s/github/help-me-mom/ng-mocks-sandbox/tree/tests?file=/src/examples/MockHostDirective/test.spec.ts&initialpath=%3Fspec%3DMockHostDirective)
- [Try it on StackBlitz](https://stackblitz.com/github/help-me-mom/ng-mocks-sandbox/tree/tests?file=src/examples/MockHostDirective/test.spec.ts&initialpath=%3Fspec%3DMockHostDirective)

```ts title="https://github.com/help-me-mom/ng-mocks/blob/master/examples/MockHostDirective/test.spec.ts"
import {
Component,
Directive,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';

import {
MockBuilder,
MockDirective,
MockRender,
ngMocks,
} from 'ng-mocks';

@Directive({
selector: 'host',
standalone: true,
})
class HostDirective {
@Input() input?: string;
@Output() output = new EventEmitter<void>();

public hostMockHostDirective() {}
}

@Component({
selector: 'target',
hostDirectives: [
{
directive: HostDirective,
inputs: ['input'],
outputs: ['output'],
},
],
template: 'target',
})
class TargetComponent {
public targetMockHostDirective() {}
}

describe('MockHostDirective', () => {
describe('TestBed', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [MockDirective(HostDirective)],
declarations: [TargetComponent],
}),
);

it('mocks host directives', () => {
const fixture = TestBed.createComponent(TargetComponent);

const directive = ngMocks.findInstance(fixture, HostDirective);
expect(directive).toBeDefined();
});
});

describe('MockBuilder', () => {
beforeEach(() => MockBuilder(TargetComponent, HostDirective));

it('mocks host directives', () => {
MockRender(TargetComponent, { input: 'test' });

const directive = ngMocks.findInstance(HostDirective);
expect(directive.input).toEqual('test');
});
});

describe('MockBuilder:shallow', () => {
beforeEach(() =>
MockBuilder().mock(TargetComponent, { shallow: true }),
);

it('mocks host directives', () => {
MockRender(TargetComponent, { input: 'test' });

const directive = ngMocks.findInstance(HostDirective);
expect(directive.input).toEqual('test');
});
});
});
```
2 changes: 2 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ module.exports = {
'guides/directive-structural',
'guides/directive-structural-context',
'guides/directive-standalone',
'guides/host-directive',
'guides/pipe',
'guides/pipe-standalone',
'guides/view-child',
Expand All @@ -171,6 +172,7 @@ module.exports = {
collapsed: false,
items: [
'guides/mock/directive-structural-let-of',
'guides/mock/host-directive',
'guides/mock/activated-route',
'guides/mock/dynamic-components',
],
Expand Down
96 changes: 96 additions & 0 deletions examples/MockHostDirective/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Component,
Directive,
EventEmitter,
Input,
Output,
VERSION,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';

import {
MockBuilder,
MockDirective,
MockRender,
ngMocks,
} from 'ng-mocks';

@Directive(
{
selector: 'host',
standalone: true,
} as never /* TODO: remove after upgrade to a14 */,
)
class HostDirective {
@Input() input?: string;
@Output() output = new EventEmitter<void>();

public hostMockHostDirective() {}
}

@Component(
{
selector: 'target',
hostDirectives: [
{
directive: HostDirective,
inputs: ['input'],
outputs: ['output'],
},
],
template: 'target',
} as never /* TODO: remove after upgrade to a15 */,
)
class TargetComponent {
public targetMockHostDirective() {}
}

describe('MockHostDirective', () => {
if (Number.parseInt(VERSION.major, 10) < 15) {
it('needs a15+', () => {
expect(true).toBeTruthy();
});

return;
}

describe('TestBed', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [MockDirective(HostDirective)],
declarations: [TargetComponent],
}).compileComponents(),
);

it('mocks host directives', () => {
const fixture = TestBed.createComponent(TargetComponent);

const directive = ngMocks.findInstance(fixture, HostDirective);
expect(directive).toBeDefined();
});
});

describe('MockBuilder', () => {
beforeEach(() => MockBuilder(TargetComponent, HostDirective));

it('mocks host directives', () => {
MockRender(TargetComponent, { input: 'test' });

const directive = ngMocks.findInstance(HostDirective);
expect(directive.input).toEqual('test');
});
});

describe('MockBuilder:shallow', () => {
beforeEach(() =>
MockBuilder().mock(TargetComponent, { shallow: true }),
);

it('mocks host directives', () => {
MockRender(TargetComponent, { input: 'test' });

const directive = ngMocks.findInstance(HostDirective);
expect(directive.input).toEqual('test');
});
});
});

0 comments on commit b8414a4

Please sign in to comment.