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

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-toggle
  • MatSliderModule:对应mat-slider和matSliderThumb
  • MatTableModule:对应mat-table系列表格组件
  • MatIconModule:对应mat-icon
  • MatButtonModule:对应mat-flat-button、mat-icon-button
  • MatTooltipModule:对应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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 00:37:14