I have written a component test for a simple custom form control component 'my-input' (implementing the ControlValueAccessor interface) which only contains an input field.
In the component test I'm handing over a value for the inner input field via 'ngModel' and I'm expecting the value to appear in the input field, but it doesn't.
my-input.component.html
<input [(ngModel)]="value" />
my-input.component.ts
import { Component, forwardRef } from '@angular/core';
import {
ControlValueAccessor,
FormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
@Component({
selector: 'my-input',
templateUrl: './my-input.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MyInputComponent),
multi: true,
},
],
standalone: true,
imports: [FormsModule],
})
export class MyInputComponent implements ControlValueAccessor {
inputValue?: string;
onChange: any = () => {};
onTouch: any = () => {};
public get value(): string {
return this.inputValue || '';
}
public set value(value: string) {
this.inputValue = value;
this.onChange(this.inputValue);
this.onTouch(this.inputValue);
}
writeValue(value: string) {
this.inputValue = value;
this.onChange(this.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouch = fn;
}
}
my-input.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyInputComponent } from './my-input.component';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
describe('MyInputComponent', () => {
@Component({
imports: [MyInputComponent, FormsModule],
standalone: true,
template: `<my-input [(ngModel)]="someValue"></my-input>`,
})
class TestHostComponent {
someValue?: string;
}
let hostComponent: TestHostComponent;
let hostFixture: ComponentFixture<TestHostComponent>;
let componentDe: DebugElement;
beforeEach(async () => {
await TestBed.compileComponents();
});
beforeEach(() => {
hostFixture = TestBed.createComponent(TestHostComponent);
hostComponent = hostFixture.componentInstance;
componentDe = hostFixture.debugElement;
hostFixture.detectChanges();
});
describe('when setting a value via ngModel', () => {
it('should set its value correctly', async () => {
const inputElement: HTMLInputElement = componentDe.query(
By.css('input')
).nativeElement;
expect(inputElement).toBeDefined();
expect(inputElement.value).toEqual('');
hostComponent.someValue = 'Hello';
hostFixture.detectChanges();
await hostFixture.whenStable();
expect(inputElement.value).toEqual('Hello');
});
});
});
I tested 'my-input' component in an Angular app, which worked just fine:
main.ts
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { MyInputComponent } from './my-input-component/my-input.component';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [MyInputComponent, FormsModule],
template: `
<h1>Hello from {{ name }}!</h1>
<my-input [(ngModel)]="name"></my-input>
`,
})
export class App {
name = 'Angular';
}
bootstrapApplication(App);
You can try it on Stackblitz here: https://stackblitz.com/edit/stackblitz-starters-2uoepe?file=src%2Fmy-input-component%2Fmy-input.component.spec.ts
For some reason, I need to call hostFixture.detectChanges(); await hostFixture.whenStable();
twice, to make the test succeed.
it('should set its value correctly', async () => {
const inputElement: HTMLInputElement = componentDe.query(
By.css('input')
).nativeElement;
expect(inputElement).toBeDefined();
expect(inputElement.value).toEqual('');
hostComponent.someValue = 'Hello';
hostFixture.detectChanges();
await hostFixture.whenStable();
hostFixture.detectChanges();
await hostFixture.whenStable();
expect(inputElement.value).toEqual('Hello');
});