Angular 16组件DOM操作单元测试报错解决方案咨询
解决Angular 16 Textarea组件单元测试DOM操作报错问题
问题背景
开发Angular 16 UI库的textarea组件时,组件DOM操作逻辑运行正常,但单元测试中访问myTextarea.nativeElement时触发报错:
TypeError: Cannot read properties of undefined (reading 'nativeElement')
at UserContext.apply (projects/lib/src/lib/components/ca-textarea/ca-textarea.component.spec.ts:76:30)
组件TS代码
@ViewChild('myTextarea') myTextarea!: ElementRef<HTMLTextAreaElement>; @Input() placeholderLabel?: string = 'label'; @Input() helperText?: string; @Input() formGroup?: FormGroup; @Input() formControlName?: string; @Input() name: string; @Input() disabled: boolean = false; @Input() isWrongInput: boolean = false; // deprecated @Input() hasError: boolean = false; showHelperText: boolean = false; showName: boolean = false; tempPlaceholder: string = ''; ngOnInit(): void { if (this.helperText) { this.showHelperText = true; } this.tempPlaceholder = this.placeholderLabel; } onFocus() { if (!this.disabled) { this.showName = true; this.placeholderLabel = ''; } } onBlur(event: any) { if (!this.disabled) { if (event.target.value == "") { this.showName = false; this.placeholderLabel = this.tempPlaceholder; } } } autoResizeTextarea() { const textarea = this.myTextarea.nativeElement; textarea.style.height = 'auto'; // Imposta l'altezza su 'auto' per calcolare la dimensione corretta textarea.style.height = `${textarea.scrollHeight}px`; // Imposta l'altezza basata sulla dimensione del contenuto if (textarea.style.height !== '44px') { textarea.style.lineHeight = '24px'; this.autoResizeTextarea(); if (textarea.style.height === '80px') { textarea.style.lineHeight = '52px'; } } if (textarea.style.height === '44px' || textarea.style.height === undefined) { textarea.style.lineHeight = '38px'; } }
单元测试代码
describe('CaInputTextComponent', () => { let component: CaTextAreaComponent; let fixture: ComponentFixture<CaTextAreaComponent>; beforeEach(() => { TestBed.configureTestingModule({ imports: [ CaTextAreaComponent, ], }); fixture = TestBed.createComponent(CaTextAreaComponent); component = fixture.componentInstance; }); //Test per verificare la creazione di un componente it('Dovrebbe creare il componente', () => { expect(component).toBeTruthy(); }); //Test per verificare se viene mostrata la label nel template it('Dovrebbe mostrare la label nel template', () => { component.placeholderLabel = 'Test'; fixture.detectChanges(); const labelElement = fixture.nativeElement.querySelector('.input'); expect(labelElement.placeholder).toContain('Test'); }); //Test click evento onFocus it('Dovrebbe emettere evento click con il corrretto valore', () => { const labelElement = fixture.nativeElement.querySelector('.input'); component.placeholderLabel = 'Test'; component.onFocus(); expect(component.placeholderLabel).toEqual(''); expect(labelElement.placeholder).toEqual(''); }); //Test click evento onBlur it('Dovrebbe emettere evento click con il corrretto valore', () => { const labelElement = fixture.nativeElement.querySelector('.input'); const event = { target: { value: '' } }; component.onBlur(event); fixture.detectChanges(); expect(component.showName).toEqual(false); expect(labelElement.placeholder).toBe(component.tempPlaceholder); }); //Test click evento onBlur it('Dovrebbe emettere evento click con il corrretto valore', () => { component.helperText = 'test'; component.ngOnInit(); fixture.detectChanges(); expect(component.helperText).toEqual('test'); expect(component.showHelperText).toEqual(true); }); it('should resize textarea properly', () => { component.myTextarea.nativeElement.value = 'Sss'; component.autoResizeTextarea(); expect(component.myTextarea.nativeElement.style.height).toBe(`${component.myTextarea.nativeElement.scrollHeight}px`); // Add other expectations based on your specific requirements }); });
解决方案
1. 触发变更检测初始化ViewChild
Angular的@ViewChild是在组件模板渲染完成、变更检测执行后才会赋值给组件属性的。当前测试中,创建组件实例后未触发变更检测,导致myTextarea为undefined。
修改报错的测试用例,添加fixture.detectChanges():
it('should resize textarea properly', () => { // 触发变更检测,让Angular绑定ViewChild到DOM元素 fixture.detectChanges(); component.myTextarea.nativeElement.value = 'Sss'; component.autoResizeTextarea(); expect(component.myTextarea.nativeElement.style.height).toBe(`${component.myTextarea.nativeElement.scrollHeight}px`); });
2. 优化其他依赖DOM的测试用例
对于依赖DOM元素的测试(如onFocus、onBlur测试),需先触发变更检测确保DOM元素存在并同步视图:
// 示例:优化onFocus测试 it('Dovrebbe emettere evento click con il corrretto valore', () => { component.placeholderLabel = 'Test'; // 先触发变更检测渲染模板 fixture.detectChanges(); const labelElement = fixture.nativeElement.querySelector('.input'); component.onFocus(); // 再次触发变更检测更新视图 fixture.detectChanges(); expect(component.placeholderLabel).toEqual(''); expect(labelElement.placeholder).toEqual(''); });
3. 隔离测试:手动模拟ElementRef(可选)
如果想脱离真实DOM环境测试逻辑,可以手动创建模拟的ElementRef并赋值给组件:
it('should resize textarea properly (isolated test)', () => { // 创建模拟textarea元素 const mockTextarea = document.createElement('textarea'); mockTextarea.value = 'Sss'; // 手动赋值给组件的myTextarea component.myTextarea = new ElementRef(mockTextarea); component.autoResizeTextarea(); expect(mockTextarea.style.height).toBe(`${mockTextarea.scrollHeight}px`); });
4. 修复递归逻辑的潜在问题
组件中autoResizeTextarea存在递归调用,可能导致测试时栈溢出。建议添加终止条件或重写逻辑避免无限递归:
autoResizeTextarea() { const textarea = this.myTextarea.nativeElement; textarea.style.height = 'auto'; const newHeight = `${textarea.scrollHeight}px`; // 仅当高度变化时才更新,避免递归 if (textarea.style.height !== newHeight) { textarea.style.height = newHeight; if (textarea.style.height !== '44px') { textarea.style.lineHeight = '24px'; if (textarea.style.height === '80px') { textarea.style.lineHeight = '52px'; } } else { textarea.style.lineHeight = '38px'; } } }
内容的提问来源于stack exchange,提问作者Salvatore Lorello
相关产品推荐
相关产品推荐

