Angular独立组件测试中detectChanges()失效(实现ControlValueAccessor)
自定义表单组件测试问题:ngModel传值在测试中不生效
我实现了一个实现ControlValueAccessor接口的自定义表单组件my-input,仅包含一个输入框。在实际Angular应用中使用正常,但编写组件测试时,通过ngModel传递值后,输入框并未显示预期值。
相关代码
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'); }); }); });
实际应用代码(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);
问题原因及解决方案
1. writeValue方法实现问题
原writeValue方法直接修改inputValue,没有触发组件的value setter,导致模板绑定的[(ngModel)]="value"无法感知值的变化,视图未更新。
修复后的writeValue方法:
writeValue(value: string) { this.value = value; // 通过setter赋值,触发视图更新和表单变更通知 }
2. 测试中DOM元素查找不准确
测试代码直接从宿主组件根元素查找input,虽能找到,但更可靠的方式是先定位到my-input组件实例,再查找内部输入框,避免DOM结构变化导致的查找失败。
修改后的测试代码片段:
it('should set its value correctly', async () => { // 先找到my-input组件的debugElement const myInputDe = componentDe.query(By.directive(MyInputComponent)); const inputElement: HTMLInputElement = myInputDe.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'); });
额外优化(可选)
为避免手动管理变更检测,可将inputValue改为Angular信号(Signal),自动触发视图更新:
import { signal } from '@angular/core'; // ... inputValue = signal<string>(''); public get value(): string { return this.inputValue(); } public set value(value: string) { this.inputValue.set(value); this.onChange(value); this.onTouch(value); } writeValue(value: string) { this.inputValue.set(value); this.onChange(value); }
内容的提问来源于stack exchange,提问作者Stephan
相关产品推荐
相关产品推荐

