Angular Test Error - NullInjectorError: No provider for TrimInputDirective
Asked Answered
H

2

10

I created an Angular Directive, that uses CSS selectors to automatically trim inputs in my application, it looks like so...

import { Directive, HostListener, forwardRef } from '@angular/core';
import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export const TRIM_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => TrimInputDirective),
  multi: true
};

/**
 * The trim accessor for writing trimmed value and listening to changes that is
 * used by the {@link NgModel}, {@link FormControlDirective}, and
 * {@link FormControlName} directives.
 */
/* tslint:disable */
@Directive({
  selector: `
    input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
    [formControlName],

    input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
    [formControl],

    input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
    [ngModel],

    textarea
    :not([readonly])
    :not(.ng-trim-ignore)
    [formControlName],

    textarea
    :not([readonly])
    :not(.ng-trim-ignore)
    [formControl],

    textarea
    :not([readonly])
    :not(.ng-trim-ignore)[ngModel],
    :not([readonly])
    :not(.ng-trim-ignore)
    [ngDefaultControl]'
  `,
  providers: [ TRIM_VALUE_ACCESSOR ]
})
/* tslint:enable */
export class TrimInputDirective extends DefaultValueAccessor {

  protected _onTouched: any;

  /**
   * ngOnChange - Lifecycle hook that is called when any data-bound property of a directive changes.
   * @param {string} val - trim value onChange.
   */
  @HostListener('input', ['$event.target.value'])
  public ngOnChange = (val: string) => {
    this.onChange(val.trim());
  }

  /**
   * applyTrim - trims the passed value
   * @param {string} val - passed value.
   */
  @HostListener('blur', ['$event.target.value'])
  public applyTrim(val: string) {
    this.writeValue(val.trim());
    this._onTouched();
  }

  /**
   * writeValue - trims the passed value
   * @param {any} value - passed  value.
   */
  public writeValue(value: any): void {
    if (typeof value === 'string') {
      value = value.trim();
    }

    super.writeValue(value);
  }

  /**
   * registerOnTouched Registers a callback function that should be called when the control receives a blur event.
   * @param {function} fn - The user information.
   */
  public registerOnTouched(fn: any): void {
    this._onTouched = fn;
  }
}

Now being a good developer I must right some unit tests... so I start to put a file together, here it is

import {Component} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {TrimInputDirective} from './trim-input.directive';

import {expect} from 'chai';

@Component({
  selector: 'my-directive-test-component',
  template: ''
})
class TestComponent {
}

describe('Trim Directive', () => {
  let fixture: ComponentFixture<TestComponent>;
  let inputDebugElement: any;
  let directive: TrimInputDirective;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        TestComponent,
        TrimInputDirective
      ],
      providers: []
    }).overrideComponent(TestComponent, {
      set: {
        template: '<input type="text">'
      }
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      inputDebugElement = fixture.debugElement.query(By.css('input'));
      directive = inputDebugElement.injector.get(TrimInputDirective);
    });
  }));

  it('should trim the input', () => {
    directive.ngOnChange('     1234.56     ')
    expect('1234.56').to.be('1234.56'); // I know this isn't the correct test... I will amend this
  });
});

Now I wish to run my tests just to make sure that the setup in the spec file is correct, but I get the following error:

HeadlessChrome 0.0.0 (Mac OS X 10.12.6) Trim Directive "before each" hook for "should trim the input" FAILED Uncaught (in promise): Error: StaticInjectorError(DynamicTestModule)[TrimInputDirective]:
StaticInjectorError(Platform: core)[TrimInputDirective]: NullInjectorError: No provider for TrimInputDirective! Error: StaticInjectorError(DynamicTestModule)[TrimInputDirective]:

I don't understand why I get this error, why do I have to provide the directive? I don't think this is necessary, also if I must provide what do I provide? Providing the actual directive doesn't work / resolve the error? I am very confused. If anyone can tell me how to resolve the issue or why I get it I would be most appreciative.

Please note that this is a legacy Angular App and was built before the AngularCLI was available. So it is a little unorthodox (for example it isn't using Jasmin).

Harbaugh answered 1/6, 2018 at 8:51 Comment(0)
G
3

1) You don't need to provide your directive, you just need to declare it in the TestingModule. Then, it will be used in the template with the corresponding selectors.

2) Your selector does not correspond to the one used on your input. Remove formControlName if you want it to apply to all inputs of certain types or change your test.

input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
    [formControlName],
    ^^^^^^^^^^^^^^^^^^ 

3) The Directive gets triggered on certain events. You need to simulate those events to see an effect. Have a look at this simplified example. (Stackblitz)

@Directive({
  selector: `
    input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
  `
})
export class TrimInputDirective {
  constructor(private el: ElementRef) { }

  @HostListener('blur') onLeave() {
   if (this.el.nativeElement.value)
    this.el.nativeElement.value = this.el.nativeElement.value.trim();
  }

}

And the test:

describe('Trim Directive', () => {
  let fixture: ComponentFixture<TestComponent>;
  let inputDebugElement: any;
  let directive: TrimInputDirective;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        TestComponent,
        TrimInputDirective
      ],
      imports: [FormsModule],
      providers: []
    }).overrideComponent(TestComponent, {
      set: {
        template: '<input type="text">'
      }
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      inputDebugElement = fixture.debugElement.query(By.css('input')).nativeElement;
                                                                      ^^^^^^^^^^^^
    }); 
  }));

  it('should trim the input', () => {
    inputDebugElement.value = '     1234.56     ';
    inputDebugElement.dispatchEvent(new Event('blur'));
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    fixture.detectChanges();
    expect(inputDebugElement.value).toBe('1234.56'); 
  });
});
Gogetter answered 4/6, 2018 at 14:26 Comment(0)
P
3

You have to provide directive in your TestComponent something like this

@Component({
  selector: 'my-directive-test-component',
  template: '',
  providers: [ TrimInputDirective ]
})
class TestComponent {
}
Preference answered 4/6, 2018 at 14:13 Comment(2)
I tried that and I was then asked for further providers, naturally that was the first thing I tried. Something is wrong with my implementation I thinkHarbaugh
Directives should be declared not provided. angular.io/guide/…Fritzie
G
3

1) You don't need to provide your directive, you just need to declare it in the TestingModule. Then, it will be used in the template with the corresponding selectors.

2) Your selector does not correspond to the one used on your input. Remove formControlName if you want it to apply to all inputs of certain types or change your test.

input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
    [formControlName],
    ^^^^^^^^^^^^^^^^^^ 

3) The Directive gets triggered on certain events. You need to simulate those events to see an effect. Have a look at this simplified example. (Stackblitz)

@Directive({
  selector: `
    input
    :not([type=checkbox])
    :not([type=radio])
    :not([type=password])
    :not([readonly])
    :not(.ng-trim-ignore)
  `
})
export class TrimInputDirective {
  constructor(private el: ElementRef) { }

  @HostListener('blur') onLeave() {
   if (this.el.nativeElement.value)
    this.el.nativeElement.value = this.el.nativeElement.value.trim();
  }

}

And the test:

describe('Trim Directive', () => {
  let fixture: ComponentFixture<TestComponent>;
  let inputDebugElement: any;
  let directive: TrimInputDirective;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        TestComponent,
        TrimInputDirective
      ],
      imports: [FormsModule],
      providers: []
    }).overrideComponent(TestComponent, {
      set: {
        template: '<input type="text">'
      }
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      inputDebugElement = fixture.debugElement.query(By.css('input')).nativeElement;
                                                                      ^^^^^^^^^^^^
    }); 
  }));

  it('should trim the input', () => {
    inputDebugElement.value = '     1234.56     ';
    inputDebugElement.dispatchEvent(new Event('blur'));
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    fixture.detectChanges();
    expect(inputDebugElement.value).toBe('1234.56'); 
  });
});
Gogetter answered 4/6, 2018 at 14:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.