Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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(); }
  }
}
like image 258
Sasha Avatar asked Nov 16 '18 20:11

Sasha


2 Answers

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.

like image 62
dmcgrandle Avatar answered Sep 24 '22 19:09

dmcgrandle


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

like image 44
André Roggeri Campos Avatar answered Sep 21 '22 19:09

André Roggeri Campos