Angular 22 的 formField 测试报找不到 ControlValueAccessor,检查 ng-mocks 版本
Angular 22.1.5 原生 formField 输入在旧 ng-mocks 中可能被提前判定缺少 CVA。说明 14.18.0 支持与 DOM input、blur 的暂时对照方式。
原生 input 已用 [formField] 绑定 signal form,ngMocks.change() 却在写值之前抛出错误:
Cannot find ControlValueAccessor on the element.
如果组合是 Angular 22.1.5 与 ng-mocks 14.17.5 或 14.17.6,这不一定是组件漏写了 CVA。signal form 可以暴露 NgControl,但它不必通过旧式 NgControl.valueAccessor 驱动原生输入。旧 helper 在这里提前抛错,没有进入 input 事件路径;ngMocks.touch() 也会遇到相近的查找问题。
原生 input 可以走真实事件
先确认测试保留了真实的 FormField,模板绑定的字段和组件模型确实对应。若原有测试使用 MockBuilder,需同时按项目要求保留 signal forms 需要的根服务;把这些服务 mock 掉后,事件本身也可能失败,那是另一层测试配置问题。
对于原生文本 input,可暂时用 DOM 值与事件比较。下面的片段放在已经配置好测试模块的异步 it 中;被测 TargetComponent 的 model 为 { name: string } signal,f = form(model),模板为 <input [formField]="f.name" />:
const fixture = MockRender(TargetComponent);
await fixture.whenStable();
const component = fixture.point.componentInstance;
const input = ngMocks.find<HTMLInputElement>("input");
input.nativeElement.value = "Ada";
ngMocks.trigger(input, "input");
ngMocks.trigger(input, "blur");
fixture.detectChanges();
await fixture.whenStable();
expect(component.model().name).toBe("Ada");
expect(component.f.name().dirty()).toBe(true);
expect(component.f.name().touched()).toBe(true);
这里使用测试文件已有的 MockRender、ngMocks 和断言环境。一个页面有多个输入框时,用实际字段对应的 selector 或字段引用定位,不能默认第一个 input 就是目标。
只改 nativeElement.value 不会自动更新 signal 模型。input 事件负责让字段读取新值,blur 则覆盖触摸状态;只断言 DOM 中出现了文字,会漏掉模型根本没有变化的情况。
14.18.0 包含 helper 的 signal form 支持
正式版 ng-mocks 14.18.0 已包含 change、touch 对 signal form 的处理。查找可以继续进入合适的绑定或原生事件路径,不再把“有 NgControl、没有旧式 CVA”一律当成无效控件。
通过项目原来的依赖更新方式采用兼容版本,保留 Angular、测试框架和相关 mock 库的版本关系,再重新运行原来的 helper 调用。没有必要给所有原生 input 人工添加一个 CVA,也不应为了通过测试把 FormField 改成空 mock。
自定义 FormValueControl、保留 CVA 的子组件和 mocked Material 控件不一定走原生 input 事件。它们可能通过模型输入、输出或注册后的 accessor 交换值,不能把上面的 DOM 片段机械套到没有 input 监听器的宿主上。
验收时同时检查模型值、显示值、dirty 与 touched 的预期变化,以及禁用、只读等原有状态。若不再抛错却仍然不更新模型,继续检查实际绑定和 accessor 注册,不能把“没有异常”当成 helper 已成功驱动控件。


