Component with ngComponentOutlet
See original GitHub issueI’m having trouble testing a component that uses ngComponentOutlet. Tests fail citing lack of component factory for Components injected via the component outlet:
HeadlessChrome 75.0.3770 (Mac OS X 10.14.1) ScreeningWizardComponent should render wizard FAILED
Error: No component factory found for WelcomeStepComponent. Did you add it to @NgModule.entryComponents?
error properties: Object({ ngComponent: Function, ngDebugContext: DebugContext_({ view: Object({ def: Object({ factory: Function, nodeFlags: 51041281, rootNodeFlags: 33554433, nodeMatchedQueries: 0, flags: 0, nodes: [ Object({ nodeIndex: 0, parent: null, renderParent: null, bindingIndex: 0, outputIndex: 0, checkIndex: 0, flags: 33554433, childFlags: 17486849, directChildFlags: 16831489, childMatchedQueries: 0, matchedQueries: Object({ }), matchedQueryIds: 0, references: Object({ }), ngContentIndex: null, childCount: 4, bindings: [ ], bindingFlags: 0, outputs: [ ], element: Object({ ns: '', name: 'espresso-wizard-step', attrs: [ ], template: null, componentProvider: Object({ nodeIndex: 2, parent: <circular reference: Object>, renderParent: <circular reference: Object>, bindingIndex: 0, outputIndex: 0, checkIndex: 2, flags: 49152, childFlags: 0, directChildFlags: 0, childMatchedQueries: 0, matchedQueries: Object, matchedQueryIds: 0, references: Object, ngContentIndex: -1, childCount: 0, bindings: ...
screening-wizard.component.html
<espresso-wizard-container
#screeningWizard
[startingStep]="'wizard-step-container'">
<div espresso-wizard-steps>
<espresso-wizard-step
*ngFor="let step of steps$ | async"
label="{{step?.config?.label}}">
<ng-template [ngComponentOutlet]="step?.component"></ng-template>
</espresso-wizard-step>
</div>
</espresso-wizard-container>
<espresso-modal
#pageHelpModal
[title]="helpModalTitle | ei9Translation | async"
id="page-help-modal"
[size]="'sm'"
[modalBody]="pageHelpModalComponent">
</espresso-modal>
screening-wizard.component.ts
@Component({
selector: 'app-screening-wizard',
templateUrl: './screening-wizard.component.html',
styleUrls: ['./screening-wizard.component.scss']
})
export class ScreeningWizardComponent implements OnInit, OnDestroy {
@ViewChild('pageHelpModal') pageHelpModal: ModalComponent;
steps$: Observable<Ei9ScreeningWizardStep[]>;
pageHelpModalComponent: any;
helpModalTitle: string;
private _unsubscribe$ = new Subject<void>();
constructor(
private _screeningWizardService: Ei9ScreeningWizardService,
private _pageHelpService: PageHelpService
) {}
ngOnInit() {
this.pageHelpModalComponent = PageHelpModalComponent;
this.steps$ = this._screeningWizardService.screeningWizardUpdated$
.pipe(map((wizard: Ei9ScreeningWizard) => wizard.steps));
this._pageHelpService.openModal$
.pipe(
takeUntil(this._unsubscribe$)
)
.subscribe((name) => {
this.helpModalTitle = `myadp.pageHelp.${name}.modalTitle`;
this.pageHelpModal.openModal();
});
}
ngOnDestroy(): void {
this._unsubscribe$.next();
this._unsubscribe$.complete();
}
}
screening.module.ts
@NgModule({
imports: [
...
],
exports: [
ScreeningDetailsBarComponent
],
declarations: [
ScreeningWizardComponent,
AttachmentsStepComponent,
DocumentsStepComponent,
EmploymentStepComponent,
ReviewStepComponent,
WelcomeStepComponent,
PageHelpModalComponent,
PageHelpLinkComponent,
ScreeningDetailsBarComponent,
ScreeningWizardHeaderComponent,
ScreeningImageComponent,
Ei9LegacyContainerComponent
],
entryComponents: [
AttachmentsStepComponent,
DocumentsStepComponent,
EmploymentStepComponent,
ReviewStepComponent,
WelcomeStepComponent,
PageHelpModalComponent,
ScreeningImageComponent
],
providers: [
PageHelpService
]
})
export class Ei9ScreeningModule {}
screening-wizard.component.spec.ts
describe('ScreeningWizardComponent', () => {
let shallow: Shallow<ScreeningWizardComponent>;
beforeEach(async () => {
shallow = new Shallow(ScreeningWizardComponent, {
ngModule: Ei9ScreeningModule,
providers: [Ei9ScreeningWizardService, PageHelpService]
} as ModuleWithProviders<Ei9ScreeningModule>)
.mock(Ei9ScreeningWizardService, {screeningWizardUpdated$: of(EI9_SCREENING_WIZARD_MAP.get(EI9_SCREENING_ACTION.SECTION_2))})
.mock(ModalComponent, {openModal: (): any => undefined })
.mock(PageHelpService, {openModal$: of('mock.title')});
});
it('should render wizard', async () => {
const { findComponent } = await shallow.render('<app-screening-wizard></app-screening-wizard>', {whenStable: true});
expect(findComponent(WizardContainerComponent)).toHaveFoundOne();
});
});
Issue Analytics
- State:
- Created 4 years ago
- Comments:5 (4 by maintainers)
Top Results From Across the Web
NgComponentOutlet - Angular
Instantiates a Component type and inserts its Host View into the current View. NgComponentOutlet provides a declarative approach for dynamic component ...
Read more >Angular Ng Component Outlet Example - StackBlitz
Starter project for Angular apps that exports to the Angular CLI.
Read more >Angular:Passing data in and out of Dynamic Components ...
This variable is passed as value to the ngComponentOutlet property. myInjector is a custom injector created for the purpose of passing data to ......
Read more >Pass an input value into a ngComponentOutlet created ...
A workaround is to add a Service that initialize your component, and with ngComponentOutletInjector inject a custom Injector. One that you can doit...
Read more >NgComponentOutlet - Angular
Instantiates a single Component type and inserts its Host View into current View. NgComponentOutlet provides a declarative approach for dynamic component ...
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
I will close this issue for now but please re-open or comment here if you still have issues with this. Thanks again for posting!
Here are some notes from the PR that I though would be helpful to include in the issue chatter:
When rendering “normal” components, Angular looks for “selectors” in the template and searches in the module-tree for a component that matches the selector. In testing, we have total control over the module so we can swap out dummy components to match up with selectors and our tests are happy.
EntryComponents bypass this and are referenced directly by their class object instead of being plucked out of the module by their selectors. This can make components that render EntryComponents hard to test.
Here’s what I mean:
If we want to test
MyComponent
, we have two options:ComponentService
and render the realFooComponent
orBarComponent
. This is typically undesirable because Foo or Bar components could be complex which would require the tests forMyComponent
to provide setup/mocks/etc to satisfy Foo and Bar components requirements.ComponentService
and provide dummy entry components. 😎Here’s an example of option 2:
This means that if we want to test an EntryComponent that is provided by an external service, we will be required to mock the service that provides the component and we will have to declare a suitable dummy component to render.
To make this easier, I’ve added a
declare
feature:Shallow#declare(...declarations: Type<any>[])
This will add custom declarations to your test module and automatically make them entry components.
I originally avoided this feature because it seems easy to abuse but it seems there are pretty good reasons to have the feature.