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(core): ViewContainerRef.createComponent respects mocks #4742 #4743

Merged
merged 1 commit into from
Jan 21, 2023
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
89 changes: 89 additions & 0 deletions docs/articles/guides/mock/dynamic-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: How to mock a dynamic component
description: Mocking an Angular dynamic component
sidebar_label: Dynamic Components
---

Angular has introduced a way how to render components dynamically.
Now, it can be done via `ViewContainerRef.createComponent(DynamicComponent)`,
and components, which are rendering dynamic components, usually, look like:

```ts
@Component({
standalone: true,
selector: 'main',
template: '',
})
export class MainComponent implements OnInit {
// ViewContainerRef is needed to manager rendering
constructor(public readonly containerRef: ViewContainerRef) {}

async ngOnInit() {
// loading DynamicComponent
const { DynamicComponent } = await import('./dynamic.component');

// rendering DynamicComponent
this.containerRef.createComponent(DynamicComponent);
}
}
```

In unit tests, developers might need to mock `DynamicComponent` to relieve testing.
Their goal is to assert that `MainComponent` has rendered `DynamicComponent` under defined circumstances
and suppress what `DynamicComponent` does under the hood.

This can be achieved with help of `ng-mocks` and [`MockBuilder`](../../api/MockBuilder.md),
simply pass `DynamicComponent` as mock dependency:

```ts
beforeEach(() => MockBuilder(MainComponent, DynamicComponent));
```

In this case, `ng-mocks` will mock `DynamicComponent` and render its stub.

:::tip
`ng-mocks` intercepts the call of `ViewContainerRef.createComponent()`, not `import()`.
:::


## An example how to mock dynamic components

```ts
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { isMockOf, MockBuilder, MockRender, ngMocks } from 'ng-mocks';

import { DynamicComponent } from './dynamic.component';

@Component({
standalone: true,
selector: 'main',
template: '',
})
class MainComponent implements OnInit {
// ViewContainerRef is needed to manager rendering
constructor(public readonly containerRef: ViewContainerRef) {}

async ngOnInit() {
// loading DynamicComponent
const { DynamicComponent } = await import('./dynamic.component');

// rendering DynamicComponent
this.containerRef.createComponent(DynamicComponent);
}
}

describe('suite', () => {
beforeEach(() => MockBuilder(MainComponent, DynamicComponent));

it('loads lazy component as a mock', async () => {
// loading the MainComponent and waiting for its initialization
const fixture = MockRender(MainComponent);
await fixture.whenStable();

// asserting that DynamicComponent has been rendered
const el = ngMocks.find(DynamicComponent, undefined);
expect(el).toBeDefined();
});
});

```
6 changes: 5 additions & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,11 @@ module.exports = {
type: 'category',
label: 'How to mock',
collapsed: false,
items: ['guides/mock/directive-structural-let-of', 'guides/mock/activated-route'],
items: [
'guides/mock/directive-structural-let-of',
'guides/mock/activated-route',
'guides/mock/dynamic-components',
],
},
{
type: 'category',
Expand Down
64 changes: 44 additions & 20 deletions libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,48 @@ const resetTestingModule =
return original.call(instance);
};

// Monkey-patching ViewContainerRef.createComponent to replace dynamic imports with mocked declarations.
const patchVcrInstance = (vcrInstance: ViewContainerRef) => {
if (!(ViewContainerRef as any).ngMocksOverridesPatched) {
coreDefineProperty(ViewContainerRef, 'ngMocksOverridesPatched', true);

// istanbul ignore else
if (vcrInstance.createComponent) {
const createComponent = vcrInstance.createComponent;
const patchedCreateComponent = helperCreateClone(
createComponent,
undefined,
undefined,
function (component: any, ...createComponentArgs: any[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thisVrc: ViewContainerRef = this;
const map = coreInjector(NG_MOCKS, thisVrc.injector);

return createComponent.apply(thisVrc, [map?.get(component) ?? component, ...createComponentArgs] as any);
},
);

coreDefineProperty(vcrInstance.constructor.prototype, 'createComponent', patchedCreateComponent, true);
coreDefineProperty(vcrInstance, 'createComponent', patchedCreateComponent, true);
}
}
};

const createComponent =
(original: TestBedStatic['createComponent'], instance: TestBedStatic): TestBedStatic['createComponent'] =>
component => {
const fixture = original.call(instance, component);
try {
const vcr = fixture.debugElement.injector.get(ViewContainerRef);
patchVcrInstance(vcr);
} catch {
// nothing to do
}

return fixture as never;
};

const viewContainerInstall = () => {
const vcr: any = ViewContainerRef;

Expand All @@ -289,32 +331,14 @@ const viewContainerInstall = () => {
'__NG_ELEMENT_ID__',
helperCreateClone(ngElementId, undefined, undefined, (...ngElementIdArgs: any[]) => {
const vcrInstance = ngElementId.apply(ngElementId, ngElementIdArgs);

const createComponent = vcrInstance.createComponent;
coreDefineProperty(
vcrInstance,
'createComponent',
helperCreateClone(
createComponent,
undefined,
undefined,
(component: any, ...createComponentArgs: any[]) => {
const map = coreInjector(NG_MOCKS, vcrInstance.injector);

return createComponent.apply(vcrInstance, [
map?.get(component) ?? component,
...createComponentArgs,
] as any);
},
),
true,
);
patchVcrInstance(vcrInstance);

return vcrInstance;
}),
true,
);
}
coreDefineProperty(TestBed, 'createComponent', createComponent(TestBed.createComponent as never, TestBed as never));

coreDefineProperty(ViewContainerRef, 'ngMocksOverridesInstalled', true);
}
Expand Down
8 changes: 8 additions & 0 deletions tests-e2e/src/issue-4693/child.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';

@Component({
standalone: true,
selector: 'child',
template: 'child',
})
export class ChildComponent {}
48 changes: 48 additions & 0 deletions tests-e2e/src/issue-4693/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { isMockOf, MockBuilder, MockRender, ngMocks } from 'ng-mocks';

import { ChildComponent } from './child.component';

@Component({
standalone: true,
selector: 'target',
template: '',
})
class TargetComponent implements OnInit {
constructor(public readonly containerRef: ViewContainerRef) {}

async ngOnInit() {
const { ChildComponent } = await import('./child.component');
this.containerRef.createComponent(ChildComponent);
}
}

describe('issue-4693', () => {
describe('real', () => {
beforeEach(() => MockBuilder(TargetComponent));

it('loads lazy component', async () => {
const fixture = MockRender(TargetComponent);
await fixture.whenStable();
const el = ngMocks.find(ChildComponent);
expect(ngMocks.formatText(el)).toEqual('child');
expect(isMockOf(el.componentInstance, ChildComponent)).toEqual(
false,
);
});
});

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

it('loads lazy component as a mock', async () => {
const fixture = MockRender(TargetComponent);
await fixture.whenStable();
const el = ngMocks.find(ChildComponent);
expect(ngMocks.formatText(el)).toEqual('');
expect(isMockOf(el.componentInstance, ChildComponent)).toEqual(
true,
);
});
});
});