question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Component with ngComponentOutlet

See original GitHub issue

I’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:closed
  • Created 4 years ago
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
getsafcommented, Jun 30, 2019

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!

0reactions
getsafcommented, Jun 29, 2019

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:

@Injectable()
class ComponentService {
   getDynamicComponent() {
     return Math.random() === 1
       ? FooComponent
       : BarComponent;
   }
}
@Component({
  selector: 'foo',
  template: '<ng-container *ngComponentOutlet="componentService.getDynamicComponent()" />'
})
class MyComponent {
  constructor(public componentService: ComponentService) {}
}

If we want to test MyComponent, we have two options:

  1. Use the real ComponentService and render the real FooComponent or BarComponent. This is typically undesirable because Foo or Bar components could be complex which would require the tests for MyComponent to provide setup/mocks/etc to satisfy Foo and Bar components requirements.
  2. Mock the ComponentService and provide dummy entry components. 😎

Here’s an example of option 2:

describe('option 2', () => {
  let shallow: Shallow<MyComponent>;
  @Component({selector: 'dummy', template: '<i></i>'})
  class DummyComponent {}

  beforeEach(() => {
    shallow = new Shallow(MyComponent, MyModule)
      .declare(DummyComponent) // <-- New feature!
      // We cannot mock the DummyComponent because the getDynamicComponent method below
      // will return the *REAL* component so the *actual* DummyComponent must exist in our test setup!
      .dontMock(DummyComponent)
      .mock(ComponentService, {getDynamicComponent: () => DummyComponent});
  });

  it('renders the component from the ComponentSevice', async () => {
    const {find} = await shallow.render();

    expect(find(DummyComponent)).toHaveFoundOne();
  });
});

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.

Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found