组件Dom测试
组件不仅仅是一个类,它还会与 DOM 以及其它组件进行交互。我们要知道组件是否能正确渲染、是否响应用户输入和手势,或者是否集成到它的父组件和子组件中,我们需要借助TestBed
的其它特性和其它的测试辅助函数。
describe('BannerComponent (with beforeEach)', () => {
let component: BannerComponent; // 要测试的组件BannerComponent
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
// 声明要测试的组件
declarations: [BannerComponent]
});
// TestBed.createComponent 创建BannerComponent组件实例,返回一个`ComponentFixture` 对象
fixture = TestBed.createComponent(BannerComponent);
// ComponentFixture.componentInstance 拿到 BannerComponent实例
component = fixture.componentInstance;
});
it('should create', () => {
// 断言组件被定义了
expect(component).toBeDefined();
});
// ComponentFixture.nativeElement 获取组件的元素,并查找预期的文本。
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
});
createComponent()
TestBed.createComponent()
会创建 BannerComponent 的实例,它把一个对应元素添加到了测试运行器的 DOM 中,并返回一个ComponentFixture
对象。
const fixture: ComponentFixture<BannerComponent> = TestBed.createComponent(BannerComponent);
ComponentFixture
ComponentFixture 是一个测试挽具,用于与所创建的组件及其对应的元素进行交互。
可以通过测试夹具(fixture)访问组件实例,并用 Jasmine 的期望断言来确认它是否存在:
const component = fixture.componentInstance;
expect(component).toBeDefined();
ComponentFixture类提供了很多特别有用的属性和方法供我们在测试中使用。
class ComponentFixture<T> {
constructor(componentRef: ComponentRef<T>, ngZone: NgZone, _autoDetect: boolean)
// 与该组件的根元素关联的 DebugElement。【常用】
debugElement: DebugElement
// 根组件类的实例。【常用】
componentInstance: T
// 组件根部的原生元素。
nativeElement: any
// 组件的根部元素的ElementRef
elementRef: ElementRef
// 组件的 ChangeDetectorRef
changeDetectorRef: ChangeDetectorRef
componentRef: ComponentRef<T>
ngZone: NgZone | null
// 触发组件的变更检测周期。【常用】
detectChanges(checkNoChanges: boolean = true): void
// 进行变更检测以确保没有更改。
checkNoChanges(): void
// 设置夹具(fixture)是否应自动检测变化。(还运行一次detectChanges,以便检测到任何现有的更改。)
autoDetectChanges(autoDetect: boolean = true)
// 返回此夹具(fixture)当前是否稳定或者是否具有尚未完成的异步任务。
isStable(): boolean
// Get a promise that resolves when the fixture is stable.
// 返回值类型:Promise<any>。
// 当事件已触发异步活动或异步变更检测后,可用此方法继续执行测试。
whenStable(): Promise<any>
// Get a promise that resolves when the ui state is stable following animations.
whenRenderingDone(): Promise<any>
// 触发组件的销毁。
destroy(): void
}
nativeElement
ComponentFixture.nativeElement
用于获取组件根部的原生元素。 当它的类型是HTMLElement 或其派生类之一时,我们可以用标准的 HTML querySelector
深入了解元素树。
it('should have <p> with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p')!;
expect(p.textContent).toEqual('banner works!');
});
DebugElement
nativeElement的属性依赖于其运行时环境。在非浏览器平台上运行这些测试时,那些平台上可能没有 DOM,或者其模拟的 DOM 不支持完整的 HTMLElement API。
Angular 依靠 DebugElement抽象
来在其支持的所有平台上安全地工作。 Angular 不会创建 HTML 元素树,而会创建一个 DebugElement 树来封装运行时平台上的原生元素。使用nativeElement 属性解包(unwrap) DebugElement 并返回特定于平台的元素对象。
const bannerElement: HTMLElement = fixture.nativeElement;
// 上面👆🏻这行代码的最终实现:
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
DebugElement 还有另一些在测试中很有用的方法和属性:
class DebugElement extends DebugNode {
// 元素的标签名 (如果是一个元素的话)
name: string
// 元素的属性名称到属性值的映射。
// 它包括:常规属性绑定(例如 [id]="id")、主机属性绑定(例如主机:{'[id]':“id”})、内插属性绑定(例如`id="{{ value }}")
// 它不包括:输入属性绑定(例如 [myCustomInput]="value")、属性绑定(例如 [attr.role]="menu")
// property着重强调具有多少参数,或事物拥有的参数,不用它来特指某事物,只强调“有”。
properties: {[key: string]: any}
// 元素的属性名称到属性值的映射。
// attribute 着重强调某个事物固有的属性,或区别于其他事物的特征,强调“专”。
attributes: {[key: string]: string|null}
// 包含元素上的类名作为键的映射。此映射派生自 DOM 元素的 className 属性。
// 注意:此对象的值将始终为真。 如果元素上不存在类键,则该类键不会出现在 KV 对象中。
classes: {[key: string]: boolean}
// DOM元素的内联样式。如果底层 DOM 元素上没有样式属性,则为 null。
styles: {[key: string]: string | null;}
// DOM元素的childNodes,是一个DebugNode数组。
childNodes: DebugNode[]
// 组件根部的底层 DOM 元素。
nativeElement: any
// The immediate DebugElement children. Walk the tree by descending through children.
children: DebugElement[]
// 返回匹配的第一个DebugElement元素
query(predicate: Predicate<DebugElement>): DebugElement
// 返回所有匹配的DebugElement元素
queryAll(predicate: Predicate<DebugElement>): DebugElement[]
// 回所有匹配的DebugNode
queryAllNodes(predicate: Predicate<DebugNode>): DebugNode[]
// 如果元素的监听器集合中有相应的监听器,则按其名称触发事件。
triggerEventHandler(eventName: string, eventObj: any): void
// 继承自 core/DebugNode
listeners: DebugEventListener[]
parent: DebugElement | null
nativeNode: any
// 示例:https://angular.cn/guide/testing-attribute-directives
injector: Injector
componentInstance: any
context: any
references: {...}
providerTokens: any[]
}
By.css()
由于有些应用可能至少要在某些时候运行在不同的平台上。
例如,作为优化策略的一部分,该组件可能会首先在服务器上渲染,以便在连接不良的设备上更快地启动本应用。服务器端渲染器可能不支持完整的 HTML 元素 API。如果它不支持 querySelector,之前的测试就会失败。
DebugElement 提供了适用于其支持的所有平台
的查询方法。这些查询方法接受一个predicate函数,当 DebugElement 树中的一个节点与选择条件匹配时,该函数返回 true。
我们可以借助从库中为运行时平台导入By
类来创建一个predicate。这里的 By 是从浏览器平台导入的。
// By 是与 DebugElement 的查询功能一起使用的predicate。
class By {
// 匹配所有节点nodes。 比如 debugElement.query(By.all());
static all(): Predicate<DebugNode>
// 通过给定的 CSS 选择器匹配元素。比如 debugElement.query(By.css('[attribute]'));
static css(selector: string): Predicate<DebugElement>
// 匹配存在给定指令(或组件)的节点。
// 比如 debugElement.query(By.directive(MyDirective));
// 比如 debugElement.query(By.directive(MyComponent));
static directive(type: Type<any>): Predicate<DebugNode>
}
使用By.css()的测试示例:
import { By } from '@angular/platform-browser';
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
// 静态方法By.css()会用标准的CSS选择器来选择DebugElement中的各个节点。该查询会返回一个DebugElement。
const paragraphDe = bannerDe.query(By.css('p'));
// 需要使用nativeElement属性解包(unwrap)上面的返回结果才能得到 p 元素。
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
当你使用 CSS 选择器进行过滤并且只测试浏览器原生元素的属性时,用 By.css 方法可能会有点过度。用 HTMLElement 方法(比如 querySelector()
或 querySelectorAll()
)进行过滤通常更简单,更清晰。