9

I've written a reactive component by injecting the NgControl and which is decorated using the @Self decorator. My problem is related to unit testing of such component. Please look at the code below:

Disclaimer: I've quickly copied the code and made some inline changes. So, this might not be a compiler happy code.

My Reactive Component:

@Component({
  selector: 'text-input',
  templateUrl: '<input type="text" class="native_input" />'
})
class TextInput implements ControlValueAccessor {
  protected constructor(@Self() public controlDir: NgControl) {

    this.controlDir.valueAccessor = this;
  }

  // ...followed by other ControlValueAccessor methods

}

Unit Test:

describe('TextInput -', () => {

   let fixture: ComponentFixture<TextInputHost>;
   let textInput: TextInput;

   let inputElement: HTMLInputElement;

   beforeEach(async(() => {
     TestBed.configureTestingModule({
       declarations: [
         TextInput, TextInputHost
       ],
       imports: [
         FormsModule, ReactiveFormsModule
       ]
     });
  }));

  beforeEach(fakeAsync(() => {
    fixture = getTestBed().createComponent(TextInputHost);
    textInput = fixture.componentInstance.textInputComponent;
    textInput.writeValue('TestValue');
    inputElement = fixture.debugElement
       .query(By.css('native_input')).nativeElement;
    fixture.detectChanges();
    tick();
  }));

  it('Should have the initial value applied.', () => {
    expect(inputElement.value).toBe('TestValue');
  });

});

// Host component
@Component({
  template: `
   <form [formGroup]="pageForm">
    <text-input formControlName="testInput">
    </text-input>
   </form>`
})
class TextInputHost {
   @ViewChild(TextInput)
   public textInputComponent: TextInput;

   public pageForm: FormGroup = new FormGroup({
     testInput: new FormControl('Initial Value')
   });
}

Whenever I try to run the above unit test. It fails with the following error: Template parse errors: No provider for NgControl --> <text-input>....</text-input>

So I'm looking for a way to successfully run the above unit test. What, I'm looking for is a way to inject the NgControl to the TextInput component.

Gaurav
  • 1,233
  • 14
  • 23

2 Answers2

10

If anybody stumbles upon this question, I solved it using the overrideComponent() method of the TestBed class.

Note: If you think you have some other answers, please feel free to answer this.

To inject the NgControl:

beforeEach(async(() => {
     TestBed.configureTestingModule({
       declarations: [
         TextInput, TextInputHost
       ],
       imports: [
         FormsModule, ReactiveFormsModule
       ]
    })
    .overrideComponent(TextInput, {
        set: {
          providers: [
            {
              provide: NgControl,
              useValue: new FormControlDirective([], [], null, null)
            }
         ]
       }
    });
 }));
Gaurav
  • 1,233
  • 14
  • 23
  • 1
    This was the only answer that helped as I was not injecting NgControl into my constructor, just using it in the file – danwellman May 11 '21 at 10:22
  • If you are running into this issue because of a directive that injects NgControl, you can replace `.overrideComponent` with `.overrideDirective` to resolve it's dependency. – Joe Apr 27 '22 at 15:39
  • Thanks a lot, this solved my issue! Just like @danwellman above, I was using `this.injector.get(NgControl)` instead of injecting `NgControl` directly in my constructor. PS: If anyone wants to use the default `.compileComponents` instead of `overrideComponent`, the `providers`-array that's used in the `set:` above can also just be added to the `configureTestingModule`. I've only started Angular Component UnitTesting since yesterday, but it's definitely getting used to what exactly has to be mocked in order for the test to run without NullReferenceErrors.. :/ – Kevin Cruijssen Feb 01 '23 at 15:34
0

Try to add decorator @Optional before @Self in your constructor for controlDir property.

@Component({
  selector: 'text-input',
  templateUrl: '<input type="text" class="native_input" />'
})
class TextInput implements ControlValueAccessor {
  protected constructor(
     @Optional() // <- in this place
     @Self() public controlDir: NgControl) {

    this.controlDir.valueAccessor = this;
  }

  // ...followed by other ControlValueAccessor methods

}

and you can remove overrideComponent method from TestBed in your tests.
I hope it helps.

Denis Anisimov
  • 685
  • 7
  • 11