Angular Test error -- Can't bind to 'items' since it isn't a known property of 'app-dropdown'
Asked Answered
E

2

9

Just want to say that I recognize that there are many SO posts related to "Can't bind to X since it isn't a known property of Y" errors. I've looked at a ton of them, and have found a number of answers which solve the specific problems, but which I've had trouble translating to my case, which I actually think is quite general, and relates to a fundamental misunderstanding of how I should be solving my use case.

I'm creating an Angular (7) app, which I've separated into components and routes. The components are modular pieces (dropdowns, modals, buttons, whatever), and the routes are individual pages in the app. The terming is a little convoluted, because both are technically Angular components. In other words, the structure (within src/) looks like this:

- app
  - components
    - dropdown
      - dropdown.component.ts
      - dropdown.component.html
      - dropdown.component.scss
      - dropdown.component.spec.ts
  - routes
    - library
      - library.component.ts
      - library.component.html
      - library.component.scss
      - library.component.spec.ts
  ...

So I have a Library route, which is just an Angular component which imports a Dropdown component and looks like this:

import { Component, OnInit } from '@angular/core';

import { DropdownComponent } from '../../components/dropdown/dropdown.component';

@Component({
  selector: 'app-library',
  templateUrl: './library.component.html',
  styleUrls: ['./library.component.scss']
})
export class LibraryComponent implements OnInit {
  pickItem($event) {
    console.log($event.item, $event.index);
  }

  constructor() { }

  ngOnInit() {}
}

The relevant Library HTML file:

<div class="library row py4">
  <h3 class="typ--geo-bold typ--caps mb5">Style Library</h3>

  <div class="mb4" style="max-width: 35rem;">
    <p class="typ--geo-bold typ--caps">Dropdowns</p>
    <app-dropdown
      [items]="['One', 'Two', 'Three']"
      (pick)="pickItem($event)"
      title="Default dropdown"
    ></app-dropdown>
    <br />
    <app-dropdown
      [items]="['One', 'Two', 'Three']"
      (pick)="pickItem($event)"
      title="Inline dropdown"
      class="dropdown--inline"
    ></app-dropdown>
  </div>
</div>

The dropdown component is a basic component, following a similar structure. I won't paste it here unless asked, because I'm not sure it'd be additive. (Suffice it to say that it does accept items as an Input -- relevant to the below error).

This works perfectly in the browser, and builds correctly in production.

When I run my library.components.spec.ts test, though, I run into the following error:

Failed: Template parse errors:
Can't bind to 'items' since it isn't a known property of 'app-dropdown'.
1. If 'app-dropdown' is an Angular component and it has 'items' input, then verify that it is part of this module.
2. If 'app-dropdown' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. ("
    <p class="typ--geo-bold typ--caps">Dropdowns</p>
    <app-dropdown
      [ERROR ->][items]="['One', 'Two', 'Three']"
      (pick)="pickItem($event)"
      title="Default dropdown"

Here's the basic Library spec file:

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LibraryComponent } from './library.component';

describe('LibraryComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      declarations: [LibraryComponent],
    }).compileComponents();
  }));

  it('should create the component', () => {
    const fixture = TestBed.createComponent(LibraryComponent);
    const lib = fixture.debugElement.componentInstance;
    expect(lib).toBeTruthy();
  });
});

Neither the Library component nor the Dropdown component have associated modules. My understanding is that this error may relate to the fact that I haven't imported the Dropdown component into the Library module, or something, but

  • A) I'm not sure how to do that,
  • B) I'm then not sure how to incorporate that module into the app at large, and
  • C) I'm not sure what the value of that is, outside of getting the test to work.

Is there a way to get the test to work without converting these components to modules? Should all route components be modules?

EDIT

Adding The dropdown component, if relevant:

<div
  class="dropdown"
  [ngClass]="{ open: open }"
  tab-index="-1"
  [class]="class"
  #dropdown
>
  <div class="dropdown__toggle" (click)="onTitleClick(dropdown)">
    <span class="dropdown__title">{{ finalTitle() }}</span>
    <span class="dropdown__icon icon-arrow-down"></span>
  </div>
  <div *ngIf="open" class="dropdown__menu">
    <ul>
      <li
        *ngFor="let item of items; let ind = index"
        class="dropdown__item"
        [ngClass]="{ selected: selectedIndex === ind }"
        (click)="onItemClick(dropdown, item, ind)"
      >
        <span class="dropdown__label">{{ item }}</span>
      </li>
    </ul>
  </div>
</div>

And:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-dropdown',
  templateUrl: './dropdown.component.html',
  styleUrls: ['./dropdown.component.scss']
})
export class DropdownComponent implements OnInit {

  @Input() items: Array<string>;
  @Input() public selectedIndex: number = null;
  @Input() public title = 'Select one';
  @Input() public open = false;
  @Input() public class = '';

  @Output() pick = new EventEmitter();

  constructor() { }

  ngOnInit() {}

  finalTitle () {
    return typeof this.selectedIndex === 'number'
      ? this.items[this.selectedIndex]
      : this.title;
  }

  onItemClick(dropdown, item, index) {
    this._blur(dropdown);
    this.selectedIndex = index;
    this.open = false;
    this.pick.emit({ item, index });
  }

  onTitleClick(dropdown) {
    this._blur(dropdown);
    this.open = !this.open;
  }

  _blur(dropdown) {
    if (dropdown && dropdown.blur) { dropdown.blur(); }
  }
}
Emendation answered 16/11, 2018 at 20:40 Comment(3)
You should consider reviewing your tags. You have tagged both angularjs(angular 1.x) and angular (angular 2.x) as well as angular 5, 6, and 7. (the latter 3 are for problems with specific angular versions only, not generic angular 2.x+ problems). All the extra tags do is create confusion.Phenix
OK. The thought was that the solution likely applies to 2x+ and I couldn't find that tag. But I'll remove them. Sounds right.Emendation
I updated my answer to directly address the specific 3 questions you asked.Baugher
B
5

Updated to more clearly answer your 3 questions

A) There are many ways to accomplish this without creating new modules. You could simply import the DropdownComponent into your test module as André suggests below. I outline another method below in this answer that stubs the DropdownComponent, just for the purpose of testing LibraryComponent. Hopefully this answers the question of "how to do that".

B) The stub I suggest isn't a module - it is barely even a component. There is no need to incorporate the stub into the app at large, it's only purpose is to test LibraryComponent.

C) The value of this stub is only to test LibraryComponent, that is why it is a good idea to keep it as simple as possible.

This is one way of getting the test to work without converting your components to modules.

Stubbing DropdownComponent:

Below is a method to stub the DropdownComponent that you are attempting to use inside of LibraryComponent. Your error that you detail in your question is directly related to the fact that you have no 'app-dropdown' selector defined, yet your LibraryComponent is attempting to bind to it. Assuming you want to only test LibraryComponent within the library.component.spec.ts file, I suggest stubbing the DropdownComponent functionality rather than importing the actual component into the test. I created a Stackblitz to show what I mean.

From the Stackblitz, here is a snip from the library.component.spec.ts file:

@Component({
    selector: 'app-dropdown',
    template: `<h5>Dropdown</h5>`
})
class TestDropdownComponent {
    @Input() items;
    @Input() title;
    @Output() pick = new EventEmitter<any>();
}

describe('LibraryComponent', () => {
    let component: LibraryComponent;
    let fixture: ComponentFixture<LibraryComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [ RouterTestingModule ],
            declarations: [ 
                TestDropdownComponent,
                LibraryComponent
            ]
        }).compileComponents();
        fixture = TestBed.createComponent(LibraryComponent);
    }));
    it('should create the component', () => {
      const fixture = TestBed.createComponent(LibraryComponent);
      const lib = fixture.debugElement.componentInstance;
      expect(lib).toBeTruthy();
    });
});

Note - If you want to reproduce the error you detail in your question within the Stackblitz, simply comment out the line for TestDropdownComponent in the declarations of the TestBed so it looks like so:

TestBed.configureTestingModule({
    imports: [ RouterTestingModule ],
    declarations: [ 
        // TestDropdownComponent,
        LibraryComponent
    ]
}).compileComponents();

And you will once again be testing just your component without a mock/stub of DropdownComponent to bind to.

Baugher answered 16/11, 2018 at 22:3 Comment(2)
Thanks! Mocking worked like a charm. Found this article helpful to come up with a neat pattern for mocking components in tests: medium.com/@cnunciato/…Emendation
Can there be any other reason to the failure of test case with same message, despite having stubbed a component with required input params?Unfathomable
G
2

You need to import the DropdowmComponent on your test module at the declarations section

Gaylagayle answered 16/11, 2018 at 22:2 Comment(2)
Actually I would recommend against this approach. If there is later an issue introduced into DropdownComponent, then the unit test of LibraryComponent may also start to fail. I find it better to keep each unit test to just the code under test so that when things fail its immediately obvious where the problem is. Test the DropdownComponent in its own .spec file.Baugher
This is more a design decision than good/bad practice. Mocking all dependencies will lead to more focused tests, but on the other hand you'll have a lot of work to keep those stubs synced with the current implementation. (see social/solitary unit tests)Gerladina

© 2022 - 2024 — McMap. All rights reserved.