Angular 15组件单元测试报错:TypeError: Cannot read properties of undefined (reading '_hostElement')
Angular 15.1.3单元测试报错:TypeError: Cannot read properties of undefined (reading '_hostElement')
在为Angular 15.1.3项目的LootGeneratorComponent编写单元测试时,调用fixture.detectChanges()时触发以下错误:
TypeError: Cannot read properties of undefined (reading '_hostElement')
相关代码片段
loot-generator.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LootGeneratorComponent } from './loot-generator.component'; import { Clipboard } from '@angular/cdk/clipboard'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatCard, MatCardActions, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatSlider, MatSliderVisualThumb } from '@angular/material/slider'; import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; describe('LootGeneratorComponent', () => { let component: LootGeneratorComponent; let fixture: ComponentFixture<LootGeneratorComponent>; const snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']); const clipboardSpy = jasmine.createSpyObj('Clipboard', ['beginCopy']); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ FormsModule, MatRippleModule ], declarations: [ LootGeneratorComponent, MatCard, MatCardHeader, MatCardTitle, MatCardContent, MatSlideToggle, MatSlider, MatCardActions, MatSliderVisualThumb ], providers: [ {provide: MatSnackBar, useValue: snackBarSpy}, {provide: Clipboard, useValue: clipboardSpy} ] }) .compileComponents(); fixture = TestBed.createComponent(LootGeneratorComponent); component = fixture.componentInstance; fixture.detectChanges(); // 错误触发位置 }); it('should create', () => { expect(component).toBeTruthy(); }); });
loot-generator.component.ts
import { Component } from '@angular/core'; import { CoinType } from './enums/coin.enum'; import { TreasureType } from './enums/treasure.enum'; import { Loot } from './models/loot.model'; import { TreasureGenerator } from './services/treasure-generator.service'; import { Clipboard } from '@angular/cdk/clipboard'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Coin } from './models/coin.model'; import { ICoin } from './interfaces/coin.interface'; import { Treasure } from './models/treasure.model'; @Component({ selector: 'app-loot-generator', templateUrl: './loot-generator.component.html', styleUrls: ['./loot-generator.component.css'] }) export class LootGeneratorComponent { private generator: TreasureGenerator = new TreasureGenerator(); public challengeRating: number = 1; public groupLoot: boolean = false; public displayedColumns: string[] = ['coin', 'amount'] public displayTreasureColumns: string[] = ['treasure', 'name', 'value'] public loot: Loot = new Loot(); constructor( private clipboard: Clipboard, private snackBar: MatSnackBar) {} // 省略其余方法(未被调用,暂不影响) }
loot-generator.html
<mat-card class="card"> <mat-card-header> <mat-card-title>Loot Generation Options</mat-card-title> </mat-card-header> <mat-card-content> <section class="options"> <div> <mat-slide-toggle [(ngModel)]="groupLoot" name="groupLoot">Hoard Loot</mat-slide-toggle> </div> <div> <mat-slider class="slider" min="1" max="20" step="1" discrete> <input matSliderThumb [(ngModel)]="challengeRating"> </mat-slider> <label id="slider-name-label" class="slider-name-label">CR</label> <label id="slider-value-label" class="slider-value-label"> {{challengeRating}} </label> </div> </section> <mat-card-actions class="options-buttons"> <button mat-flat-button color="primary" (click)="generateLoot()">Generate</button> <button mat-flat-button color="accent" (click)="clear()">Clear</button> </mat-card-actions> </mat-card-content> </mat-card> <mat-card class="card" *ngIf="loot.coins.length > 0 || loot.treasures.length > 0"> <mat-card-header> <button mat-icon-button matTooltip="Copy to clipboard" aria-label="Copy to clipboard" (click)="onCopyToClipboardClick()"> <mat-icon>content_copy</mat-icon> </button> <button mat-icon-button matTooltip="Sell Loot" aria-label="Sell Loot" (click)="onSellClick()"> <mat-icon>sell</mat-icon> </button> </mat-card-header> <mat-card class="card" *ngIf="loot.coins.length > 0"> <table #coinTable *ngIf="loot.coins.length > 0" mat-table id="0" [dataSource]="loot.coins"> <ng-container matColumnDef="coin"> <th mat-header-cell *matHeaderCellDef> Coin </th> <td mat-cell *matCellDef="let element"> {{getCoinName(element.type)}} </td> </ng-container> <ng-container matColumnDef="amount"> <th mat-header-cell *matHeaderCellDef> Amount </th> <td mat-cell *matCellDef="let element"> {{element.amount}}</td> </ng-container> <ng-container matColumnDef="action"> <th mat-header-cell *matHeaderCellDef> Action </th> <td mat-cell *matCellDef="let element"> <button mat-flat-button>Re-Roll</button> </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table> </mat-card> <mat-card class="card" *ngIf="loot.treasures.length > 0"> <table #treasureTable *ngIf="loot.treasures.length > 0" mat-table id="1" [dataSource]="loot.treasures"> <ng-container matColumnDef="treasure"> <th mat-header-cell *matHeaderCellDef> Treasure </th> <td mat-cell *matCellDef="let element"> {{getTreasureTypeName(element.type)}} </td> </ng-container> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef> Name </th> <td mat-cell *matCellDef="let element"> {{element.name}} </td> </ng-container> <ng-container matColumnDef="value"> <th mat-header-cell *matHeaderCellDef> Value (gp)</th> <td mat-cell *matCellDef="let element"> {{element.value === 0 ? '-' : element.value}} </td> </ng-container> <ng-container matColumnDef="action"> <th mat-header-cell *matHeaderCellDef> Action </th> <td mat-cell *matCellDef="let element"> <button mat-flat-button>Re-Roll</button> </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayTreasureColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayTreasureColumns;"></tr> </table> </mat-card> <mat-card-actions> </mat-card-actions> </mat-card>
问题原因及解决方案
原因
错误根源在于测试模块中手动声明了Angular Material的组件类(如MatCard、MatSlider等),而非导入对应的Material模块。Angular Material组件依赖其所属模块提供的指令、依赖注入和初始化逻辑,单独声明组件类会导致组件初始化不完整,进而触发_hostElement未定义的错误。
解决方案
将测试模块中declarations数组内的Material组件移除,替换为导入对应的Angular Material模块。根据模板中用到的组件,需要导入以下模块:
MatCardModule:对应mat-card系列组件MatSlideToggleModule:对应mat-slide-toggleMatSliderModule:对应mat-slider和matSliderThumbMatTableModule:对应mat-table系列表格组件MatIconModule:对应mat-iconMatButtonModule:对应mat-flat-button、mat-icon-buttonMatTooltipModule:对应matTooltip指令
修改后的测试模块代码如下:
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LootGeneratorComponent } from './loot-generator.component'; import { Clipboard } from '@angular/cdk/clipboard'; import { MatSnackBar } from '@angular/material/snack-bar'; import { FormsModule } from '@angular/forms'; // 导入所需的Material模块 import { MatCardModule } from '@angular/material/card'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSliderModule } from '@angular/material/slider'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; describe('LootGeneratorComponent', () => { let component: LootGeneratorComponent; let fixture: ComponentFixture<LootGeneratorComponent>; const snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']); const clipboardSpy = jasmine.createSpyObj('Clipboard', ['beginCopy']); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ FormsModule, MatCardModule, MatSlideToggleModule, MatSliderModule, MatTableModule, MatIconModule, MatButtonModule, MatTooltipModule ], declarations: [ LootGeneratorComponent // 仅声明测试组件本身 ], providers: [ {provide: MatSnackBar, useValue: snackBarSpy}, {provide: Clipboard, useValue: clipboardSpy} ] }) .compileComponents(); fixture = TestBed.createComponent(LootGeneratorComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
修改后重新运行测试,即可解决_hostElement未定义的错误。
内容的提问来源于stack exchange,提问作者Jeremy Jones
相关产品推荐
相关产品推荐

