You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 13:44:57