组件测试工具 Component Harnesses

test-harnesses 概览

@angular/cdk/testing 提供了一些帮助测试 Angular 组件的基础设施。

@angular/cdk/testing provides infrastructure to help with testing Angular components.

组件测试工具类是一个让测试可以通过其支持的 API 与组件交互的类。每个工具的 API 都会以和用户相同的方式与一个组件进行交互。通过使用测试工具 API,测试可以防止对组件内部的更新,比如改变它的 DOM 结构。组件测试工具的思想来自常用于集成测试的 PageObject 模式。

A component harness is a class that lets a test interact with a component via a supported API. Each harness's API interacts with a component the same way a user would. By using the harness API, a test insulates itself against updates to the internals of a component, such as changing its DOM structure. The idea for component harnesses comes from the PageObject pattern commonly used for integration testing.

@angular/cdk/testing 包含用于创建和使用组件测试工具的基础设施。你可以为任何组件创建测试工具,范围从很小的可复用组件到完整的应用页面。

@angular/cdk/testing contains infrastructure for creating and using component test harnesses. You can create test harnesses for any component, ranging from small reusable widgets to full application pages.

组件工具体系支持多种测试环境。你可以在单元测试和端到端测试中使用相同的工具实现。这意味着用户只需要学习一个 API,而组件作者不需要维护单独的单元测试和端到端测试实现。

The component harness system supports multiple testing environments. You can use the same harness implementation in both unit and end-to-end tests. This means that users only need to learn one API, and component authors don't have to maintain separate unit and end-to-end test implementations.

特别是对于通用组件库,由于其组件的广泛使用,更容易从这种基础设施中受益。提供一个测试工具可以让组件的使用者编写一些不用依赖任何私有实现细节的测试。通过在一个地方捕获这些实现细节,消费者可以更轻松地更新到库的最新版本。

Common component libraries, in particular, benefit from this infrastructure due to the wide use of their components. Providing a test harness allows the consumers of a component to write tests that avoid dependencies on any private implementation details. By capturing these implementation details in a single place, consumers can more easily update to new library versions.

本文档为三类开发人员提供了指导:

This document provides guidance for three types of developers:

  1. 测试的作者

    Test authors

  2. 组件测试工具的作者

    Component harness authors

  3. 测试工具环境的作者

    Harness environment authors

由于许多开发人员只会属于这些类别之一,因此相关的 API 会在下面按开发人员类型细分成不同章节。

Since many developers fall into only one of these categories, the relevant APIs are broken out by developer type in the sections below.

测试作者就是开发人员,他们使用别人编写的组件测试工具来测试自己的应用。例如,这可能是一个使用第三方菜单组件的应用开发者,需要在单元测试中与该菜单进行交互。

Test authors are developers using component harnesses written by someone else to test their application. For example, this could be an app developer who uses a third-party menu component and needs to interact with the menu in a unit test.

ComponentHarness 是所有组件测试工具的抽象基类。每个测试工具都会扩展这个类。 所有 ComponentHarness 的子类都有一个静态属性 hostSelector,它把测试工具类与 DOM 中组件的实例相匹配。 除此之外,任何特定测试工具的 API 都是专属于其对应组件的。请参阅该组件的文档,了解如何使用特定的测试工具。

ComponentHarness is the abstract base class for all component harnesses. Every harness extends this class. All ComponentHarness subclasses have a static property, hostSelector, that matches the harness class to instances of the component in the DOM. Beyond that, the API of any given harness is specific to its corresponding component; refer to the component's documentation to learn how to use a specific harness.

这些类对应于组件工具体系的不同实现,并绑定到特定的测试环境。任何一种测试都只能导入其中一个类。 基于 Karma 的单元测试应该使用 TestbedHarnessEnvironment,而基于 Protractor 的端到端测试应该使用 ProtractorHarnessEnvironment。 其他环境会要求自定义绑定;给参见测试工具环境作者的 API,以了解有关备用测试环境的更多信息。

These classes correspond to different implementations of the component harness system with bindings for specific test environments. Any given test must only import one of these classes. Karma-based unit tests should use the TestbedHarnessEnvironment, while Protractor-based end-to-end tests should use the ProtractorHarnessEnvironment. Additional environments require custom bindings; see API for harness environment authors for more information on alternate test environments.

这些类主要用于创建一个 HarnessLoader 实例,在某些情况下,还可以用来创建 ComponentHarness 实例。

These classes are primarily used to create a HarnessLoader instance, and in certain cases, to create ComponentHarness instances directly.

TestbedHarnessEnvironment 提供了以下静态方法:

TestbedHarnessEnvironment offers the following static methods:

方法

Method

说明

Description

loader(fixture: ComponentFixture<unknown>): HarnessLoader

获取 HarnessLoader 实例,该实例以测试夹具的根元素为根。用来为夹具里面的元素创建测试工具

Gets a HarnessLoader instance for the given fixture, rooted at the fixture's root element. Should be used to create harnesses for elements contained inside the fixture

documentRootLoader(fixture: ComponentFixture<unknown>): HarnessLoader

获取 HarnessLoader 实例,该实例以 HTML 文档的根元素为根。可以用来为那些夹具外面的元素创建测试工具

Gets a HarnessLoader instance for the given fixture, rooted at the HTML document's root element. Can be used to create harnesses for elements that fall outside of the fixture

harnessForFixture<T extends ComponentHarness>(fixture: ComponentFixture<unknown>, harnessType: ComponentHarnessConstructor<T>): Promise<T>

用来直接为夹具的根元素创建一个 ComponentHarness。当使用你计划为其加载测试工具的组件启动测试时,这是必要的,因为在创建夹具时,Angular 还没有设置正确的标签名。

Used to create a ComponentHarness instance for the fixture's root element directly. This is necessary when bootstrapping the test with the component you plan to load a harness for, because Angular does not set the proper tag name when creating the fixture.

在大多数情况下,你可以使用 TestbedHarnessEnvironment.loader(fixture)beforeEach 中创建一个 HarnessLoader,然后使用该 HarnessLoader 来创建任何必要的 ComponentHarness 实例。其他方法涵盖了那些特例,如下例所示:

In most cases, you can create a HarnessLoader in the beforeEach block using TestbedHarnessEnvironment.loader(fixture) and then use that HarnessLoader to create any necessary ComponentHarness instances. The other methods cover special cases as shown in this example:

考虑一个可复用的对话框按钮组件,它在单击时会打开一个对话框,其中包含以下组件,每个组件都带有相应的工具:

Consider a reusable dialog-button component that opens a dialog on click, containing the following components, each with a corresponding harness:

  • MyDialogButton(用便利 API 组合 MyButtonMyDialog

    MyDialogButton (composes the MyButton and MyDialog with a convenient API)

  • MyButton (简单按钮组件)

    MyButton (a simple button component)

  • MyDialog (通过点击 MyDialogButton,把一个对话框附着到 document.body 上)

    MyDialog (a dialog appended to document.body by MyDialogButton upon click)

下列代码为每个组件加载了一些测试工具:

The following code loads harnesses for each of these components:

let fixture: ComponentFixture<MyDialogButton>;
let loader: HarnessLoader;
let rootLoader: HarnessLoader;

beforeEach(() => {
  fixture = TestBed.createComponent(MyDialogButton);
  loader = TestbedHarnessEnvironment.loader(fixture);
  rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});

it('loads harnesses', async () => {
  // Load a harness for the bootstrapped component with `harnessForFixture`
  dialogButtonHarness =
      await TestbedHarnessEnvironment.harnessForFixture(fixture, MyDialogButtonHarness);

  // The button element is inside the fixture's root element, so we use `loader`.
  const buttonHarness = await loader.getHarness(MyButtonHarness);

  // Click the button to open the dialog
  await buttonHarness.click();

  // The dialog is appended to `document.body`, outside of the fixture's root element,
  // so we use `rootLoader` in this case.
  const dialogHarness = await rootLoader.getHarness(MyDialogHarness);

  // ... make some assertions
});

ProtractorHarnessEnvironment 有一个提供单个静态方法的 API:

ProtractorHarnessEnvironment has an API that offers a single static method:

方法

Method

说明

Description

loader(): HarnessLoader

获取 HarnessLoader 实例,该实例以该文档的根元素为根。

Gets a HarnessLoader instance for the current HTML document, rooted at the document's root element.

虽然 Protractor 不知道怎么用夹具,但这个环境下的 API 比较简单。loader() 方法返回的 HarnessLoader 应足以加载所有必需的 ComponentHarness 实例。

Since Protractor does not deal with fixtures, the API in this environment is simpler. The HarnessLoader returned by the loader() method should be sufficient for loading all necessary ComponentHarness instances.

请注意,在各种环境中,测试工具的行为可能并不完全相同。当用户点击或输入某个元素时,真正的浏览器生成的事件序列与单元测试中生成的模拟事件序列之间总有一些区别。不过,CDK 会尽最大努力规范其行为,并模拟序列中最重要的事件。

Please note that harnesses may not behave exactly the same in all environments. There will always be some difference between the real browser-generated event sequence when a user clicks or types in an element, versus the simulated event sequence generated in unit tests. Instead, the CDK makes a best effort to normalize the behavior and simulate the most important events in the sequence.

该类的实例对应一个特定的 DOM 元素(此加载器的“根元素”),用于为该根元素下的元素创建 ComponentHarness

Instances of this class correspond to a specific DOM element (the "root element" of the loader) and are used to create ComponentHarness instances for elements under this root element.

HarnessLoader 实例有以下几种方法:

HarnessLoader instances have the following methods:

方法

Method

说明

Description

getChildLoader(selector: string): Promise<HarnessLoader>

HarnessLoader 的根元素下搜索匹配指定选择器的元素,并返回以第一个匹配元素为根的 HarnessLoader

Searches for an element matching the given selector below the root element of this HarnessLoader, and returns a new HarnessLoader rooted at the first matching element

getAllChildLoaders(selector: string): Promise<HarnessLoader[]>

行为类似于 getChildLoader,但返回一个 HarnessLoader 实例数组,给每个匹配的元素一个实例,而不是只给第一个匹配的元素

Acts like getChildLoader, but returns an array of HarnessLoader instances, one for each matching element, rather than just the first matching element

getHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T>

HarnessLoader 的根元素下面搜索指定的 ComponentHarnessHarnessPredicate 类的实例,并返回与第一个匹配元素对应的测试工具的实例

Searches for an instance of the given ComponentHarness class or HarnessPredicate below the root element of this HarnessLoader and returns an instance of the harness corresponding to the first matching element

getAllHarnesses<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T[]>

行为类似于 getHarness,但会返回一个测试工具实例的数组,给每个匹配的元素一个实例,而不只是只给第一个匹配的元素

Acts like getHarness, but returns an array of harness instances, one for each matching element, rather than just the first matching element

调用 getHarnessgetAllHarnesses 的参数可以是 ComponentHarness 的子类或 HarnessPredicateHarnessPredicate 对搜索应用了额外的限制(比如搜索一些带有特定文本的按钮等)。HarnessPredicate详细信息组件工具作者的 API 部分讨论。组件工具的作者应该在 ComponentHarness 子类中提供了一些创建 HarnessPredicate 实例的便利方法。但是,如果测试工具作者提供的 API 不够用,你也可以手动创建它们。

Calls to getHarness and getAllHarnesses can either take ComponentHarness subclass or a HarnessPredicate. HarnessPredicate applies additional restrictions to the search (e.g. searching for a button that has some particular text, etc). The details of HarnessPredicate are discussed in the API for component harness authors; harness authors should provide convenience methods on their ComponentHarness subclass to facilitate the creation of HarnessPredicate instances. However, if the harness author's API is not sufficient, they can be created manually.

默认情况下,测试工具会在读取 DOM 元素的状态之前和与 DOM 元素交互之后运行 Angular 变更检测。这虽然在大多数情况下很方便,但有时候你需要对变更检测进行更细粒度的控制。例如,你可能希望在异步操作挂起时检查某个组件的状态。在这些情况下,你可以使用 manualChangeDetection 函数来禁用对代码块的自动变更检测。例如:

By default, test harnesses will run Angular's change detection before reading the state of a DOM element and after interacting with a DOM element. While convenient in most cases, there may be times that you need finer-grained control over change detection. For example, you may want to check the state of a component while an async operation is pending. In these cases you can use the manualChangeDetection function to disable automatic handling of change detection for a block of code. For example:

it('checks state while async action is in progress', async () => {
  const buttonHarness = loader.getHarness(MyButtonHarness);
  await manualChangeDetection(async () => {
    await buttonHarness.click();
    fixture.detectChanges();
    // Check expectations while async click operation is in progress.
    expect(isProgressSpinnerVisible()).toBe(true);
    await fixture.whenStable();
    // Check expectations after async click operation complete.
    expect(isProgressSpinnerVisible()).toBe(false);
  });
});

为了支持单元测试和端到端测试,并把测试与异步行为的变化隔离开来,测试工具几乎的所有方法都是异步的,并返回一个 Promise;因此,Angular 团队建议使用 ES2017 的 async / await 语法来提高测试的可读性。

To support both unit and end-to-end tests, and to insulate tests against changes in asynchronous behavior, almost all harness methods are asynchronous and return a Promise; therefore, the Angular team recommends using ES2017 async/await syntax to improve the test readability.

注意,await 语句会阻塞测试的执行,直到相关的 Promise 被解析为止。有时,你可能希望同时执行多个动作,并等待它们全部完成,而不是按顺序执行每个动作。例如,从单个组件中读取多个属性。在这种情况下,请使用 parallel 函数来并行处理这些操作。parallel 函数与 Promise.all 工作方式类似,但同时优化了变更检测,因此它的运行次数不会太多。以下代码演示如何通过 parallel 从测试工具中读取多个属性:

Note that await statements block the execution of your test until the associated Promise resolves. Occasionally, you may want to perform multiple actions simultaneously and wait until they're all done rather than performing each action sequentially. For example, reading multiple properties off a single component. In these situations use the parallel function to parallelize the operations. The parallel function works similarly to Promise.all, while also optimizing change detection, so it is not run an excessive number of times. The following code demonstrates how you can read multiple properties from a harness with parallel:

it('reads properties in parallel', async () => {
  const checkboxHarness = loader.getHarness(MyCheckboxHarness);
  // Read the checked and intermediate properties simultaneously.
  const [checked, indeterminate] = await parallel(() => [
    checkboxHarness.isChecked(),
    checkboxHarness.isIndeterminate()
  ]);
  expect(checked).toBe(false);
  expect(indeterminate).toBe(true);
});

组件测试工具作者是那些需要维护一些可复用的 Angular 组件并希望为它创建一个测试工具的开发人员,组件用户可以在测试中使用它。 例如,可能是第三方 Angular 组件库的作者,也可能是为大型 Angular 应用维护一组通用组件的开发人员。

Component harness authors are developers who maintain some reusable Angular component, and want to create a test harness for it, that users of the component can use in their tests. For example, this could be an author of a third party Angular component library or a developer who maintains a set of common components for a large Angular application.

抽象类 ComponentHarness 是所有组件测试工具的基类。要创建自定义组件测试工具,请扩展 ComponentHarness 并实现其静态属性 hostSelectorhostSelector 属性用于标识 DOM 中与该测试工具子类相匹配的元素。在大多数情况下,hostSelector 应该与相应的 ComponentDirectiveselector 相同。例如,考虑一个简单的弹出框组件:

The abstract ComponentHarness class is the base class for all component harnesses. To create a custom component harness, extend ComponentHarness and implement the static property hostSelector. The hostSelector property identifies elements in the DOM that match this harness subclass. In most cases, the hostSelector should be the same as the selector of the corresponding Component or Directive. For example, consider a simple popup component:

@Component({
  selector: 'my-popup',
  template: `
    <button (click)="toggle()">{{triggerText}}</button>
    <div *ngIf="open" class="my-popup-content"><ng-content></ng-content></div>
  `
})
class MyPopup {
  @Input() triggerText: string;

  open = false;

  toggle() {
    this.open = !this.open;
  }
}

在这种情况下,该组件的最简测试工具如下所示:

In this case, a minimal harness for the component would look like the following:

class MyPopupHarness extends ComponentHarness {
  static hostSelector = 'my-popup';
}

虽然 ComponentHarness 子类只需要 hostSelector 属性,但大多数测试工具还应该使用静态方法 with 来生成 HarnessPredicate 实例。下面的 HarnessPredicate 部分会更详细介绍这一点。

While ComponentHarness subclasses require only the hostSelector property, most harnesses should also implement a static with method to generate HarnessPredicate instances. The HarnessPredicate section below covers this in more detail.

ComponentHarness 子类的每个实例都表示相应组件的一个特定实例。你可以用 ComponentHarness 类的 host 方法访问组件的宿主元素。

Each instance of a ComponentHarness subclass represents a particular instance of the corresponding component. You can access the component's host element via the host method from the ComponentHarness base class.

ComponentHarness 还提供了几种在组件 DOM 中定位元素的方法。它们是 locatorForlocatorForOptionallocatorForAll。但请注意,这些方法并不直接查找元素,而是会创建能寻找元素的函数。这种方式可以防止缓存对过时元素的引用。例如,当一个 ngIf 先隐藏再显示一个元素时,其结果是一个新的 DOM 元素;使用函数可以确保测试总能引用 DOM 的当前状态。

ComponentHarness additionally offers several methods for locating elements within the component's DOM. These methods are locatorFor, locatorForOptional, and locatorForAll. Note, though, that these methods do not directly find elements. Instead, they create functions that find elements. This approach safeguards against caching references to out-of-date elements. For example, when an ngIf hides and then shows an element, the result is a new DOM element; using functions ensures that tests always reference the current state of the DOM.

方法

Method

说明

Description

host(): Promise<TestElement>

返回相应组件实例的宿主元素的 Promise

Returns a Promise for the host element of the corresponding component instance.

locatorFor(selector: string): () => Promise<TestElement>

创建一个函数,该函数在被调用时会返回与指定选择器匹配的第一个元素的 Promise。如果找不到匹配的元素,Promise 就会拒绝。

Creates a function that returns a Promise for the first element matching the given selector when called. If no matching element is found, the Promise rejects.

locatorForOptional(selector: string): () => Promise<TestElement | null>

创建一个函数,该函数在被调用时会返回与指定选择器匹配的第一个元素的 Promise。如果找不到匹配的元素,则会解析成携带 nullPromise

Creates a function that returns a Promise for the first element matching the given selector when called. If no matching element is found, the Promise is resolved with null.

locatorForAll(selector: string): () => Promise<TestElement[]>

创建一个函数,它返回一个 Promise 以便在调用时返回与指定选择器匹配的所有元素的列表。

Creates a function that returns a Promise for a list of all elements matching the given selector when called.

例如,MyPopupHarness 类可以提供获取触发器和内容元素的方法,如下所示:

For example, the MyPopupHarness class discussed above could provide methods to get the trigger and content elements as follows:

class MyPopupHarness extends ComponentHarness {
  static hostSelector = 'my-popup';

  /** Gets the trigger element */
  getTriggerElement = this.locatorFor('button');

  /** Gets the content element. */
  getContentElement = this.locatorForOptional('.my-popup-content');
}

使用上述定位器方法创建的函数都返回了 TestElement 实例。TestElement 提供了许多与底层 DOM 交互的方法:

The functions created with the locator methods described above all return TestElement instances. TestElement offers a number of methods to interact with the underlying DOM:

方法

Method

说明

Description

blur(): Promise<void>

让此元素失焦。

Blurs the element.

clear(): Promise<void>

清除此元素中的文本(仅适用于 <input><textarea> )。

Clears the text in the element (intended for <input> and <textarea> only).

click(relativeX?: number, relativeY?: number): Promise<void>

单击此元素(相对于此元素左上角的指定位置)。

Clicks the element (at the given position relative to the element's top-left corner).

focus(): Promise<void>

让此元素获得焦点。

Focuses the element.

getCssValue(property: string): Promise<string>

获取此元素指定 CSS 属性的计算值。

Gets the computed value of the given CSS property for the element.

hover(): Promise<void>

悬停在此元素上方。

Hovers over the element.

sendKeys(modifiers?: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>

给此元素发送指定的按键列表(可以带修饰键)。

Sends the given list of key presses to the element (with optional modifier keys).

text(): Promise<string>

获取此元素的文本内容

Gets the text content of the element

getAttribute(name: string): Promise<string | null>

从此元素获取指定的 HTML 属性的值。

Gets the value of the given HTML attribute for the element.

hasClass(name: string): Promise<boolean>

检查此元素是否已应用了指定的类。

Checks whether the element has the given class applied.

getDimensions(): Promise<ElementDimensions>

获取此元素的尺寸。

Gets the dimensions of the element.

getProperty(name: string): Promise<any>

从此元素获取指定的 JS 属性的值。

Gets the value of the given JS property for the element.

matchesSelector(selector: string): Promise<boolean>

检查此元素是否与指定的 CSS 选择器匹配。

Checks whether the element matches the given CSS selector.

setInputValue(value: string): Promise<void>;

设置输入属性的值。

Sets the value of a property of an input.

selectOptions(...optionIndexes: number[]): Promise<void>;

在原生 select 元素中指定索引处的候选项。

Selects the options at the specified indexes inside of a native select element.

dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;

派发具有特定名称的事件。

Dispatches an event with a particular name.

TestElement 是一种抽象设计,适用于不同的测试环境(Karma,Protractor 等)。在使用测试工具时,你应该通过这个接口来进行所有的 DOM 交互。其他访问 DOM 元素的方法(例如 document.querySelector )并不适用于所有的测试环境。

TestElement is an abstraction designed to work across different test environments (Karma, Protractor, etc). When using harnesses, you should perform all DOM interaction via this interface. Other means of accessing DOM elements (e.g. document.querySelector) will not work in all test environments.

最好的做法是,你不应该把 TestElement 实例公开给测试工具的用户,除非它是由组件消费者直接定义的元素(比如宿主元素)。公开 TestElement 实例会让用户依赖组件的内部 DOM 结构。

As a best practice, you should not expose TestElement instances to users of a harness unless its an element the component consumer defines directly (e.g. the host element). Exposing TestElement instances for internal elements leads users to depend on a component's internal DOM structure.

相反,要为最终用户可能执行的操作或可能要检查的状态提供更加专用的方法。例如,MyPopupHarness 可以提供像 toggleisOpen 之类的方法:

Instead, provide more narrow-focused methods for particular actions the end-user will take or particular state they may want to check. For example, MyPopupHarness could provide methods like toggle and isOpen:

class MyPopupHarness extends ComponentHarness {
  static hostSelector = 'my-popup';

  protected getTriggerElement = this.locatorFor('button');
  protected getContentElement = this.locatorForOptional('.my-popup-content');

  /** Toggles the open state of the popup. */
  async toggle() {
    const trigger = await this.getTriggerElement();
    return trigger.click();
  }

  /** Checks if the popup us open. */
  async isOpen() {
    const content = await this.getContentElement();
    return !!content;
  }
}

较大的组件通常是由较小的组件组合而成的。你也可以在组件测试工具中反映出这种结构。ComponentHarness 上的每个 locatorFor 族方法都有一个备用签名,可以用来定位子级测试工具而不是元素。

Larger components often compose smaller components. You can reflect this structure in a component's harness as well. Each of the locatorFor methods on ComponentHarness discussed earlier has an alternate signature that can be used for locating sub-harnesses rather than elements.

方法

Method

说明

Description

locatorFor<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>): () => Promise<T>

创建一个函数,该函数在被调用时返回第一个匹配指定测试工具类型的测试工具的 Promise;如果找不到匹配的测试工具,Promise 就会拒绝。

Creates a function that returns a Promise for the first harness matching the given harness type when called. If no matching harness is found, the Promise rejects.

locatorForOptional<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>): () => Promise<T | null>

创建一个函数,该函数在被调用时返回第一个匹配指定测试工具类型的测试工具的 Promise;如果找不到匹配的工具,就会解析成携带 nullPromise

Creates a function that returns a Promise for the first harness matching the given harness type when called. If no matching harness is found, the Promise is resolved with null.

locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>): () => Promise<T[]>

创建一个函数,该函数在被调用时返回一个Promise,携带匹配到指定测试工具类型的所有测试工具列表。

Creates a function that returns a Promise for a list of all harnesses matching the given harness type when called.

例如,考虑使用上面演示过的弹出菜单来构建菜单:

For example, consider a menu build using the popup shown above:

@Component({
  selector: 'my-menu',
  template: `
    <my-popup>
      <ng-content></ng-content>
    </my-popup>
  `
})
class MyMenu {
  @Input() triggerText: string;

  @ContentChildren(MyMenuItem) items: QueryList<MyMenuItem>;
}

@Directive({
  selector: 'my-menu-item'
})
class MyMenuItem {}

MyMenu 的测试工具就可以利用 MyPopupMyMenuItem 的其他测试工具:

The harness for MyMenu can then take advantage of other harnesses for MyPopup and MyMenuItem:

class MyMenuHarness extends ComponentHarness {
  static hostSelector = 'my-menu';

  protected getPopupHarness = this.locatorFor(MyPopupHarness);

  /** Gets the currently shown menu items (empty list if menu is closed). */
  getItems = this.locatorForAll(MyMenuItemHarness);

  /** Toggles open state of the menu. */
  async toggle() {
    const popupHarness = await this.getPopupHarness();
    return popupHarness.toggle();
  }
}

class MyMenuItemHarness extends ComponentHarness {
  static hostSelector = 'my-menu-item';
}

当一个页面包含特定组件的多个实例时,你可能需要根据该组件的某些属性进行过滤,以得到一个特定的组件实例。例如,你可能想要一个带有特定文本的按钮,或一个带有特定 ID 的菜单。HarnessPredicate 可以为 ComponentHarness 的子类按一定的标准捕获它们。虽然测试作者也能手动构建 HarnessPredicate 实例,但 ComponentHarness 子类提供了一个辅助方法来为常用的过滤器构造谓词,这更容易。

When a page contains multiple instances of a particular component, you may want to filter based on some property of the component to get a particular component instance. For example, you may want a button with some specific text, or a menu with a specific ID. The HarnessPredicate class can capture criteria like this for a ComponentHarness subclass. While the test author is able to construct HarnessPredicate instances manually, it's easier when the ComponentHarness subclass provides a helper method to construct predicates for common filters.

建议在每个 ComponentHarness 子类的 with 方法中提供这个辅助方法,它返回该类的 HarnessPredicate。这让测试作者可以编写易于理解的代码,例如 loader.getHarness(MyMenuHarness.with({selector: '#menu1'}))。除了标准的 selectorancestor 选项之外,with 方法还应该添加对特定的子类有意义的其他选项。

The recommended approach to providing this helper is to create a static with method on each ComponentHarness subclass that returns a HarnessPredicate for that class. This allows test authors to write easily understandable code, e.g. loader.getHarness(MyMenuHarness.with({selector: '#menu1'})). In addition to the standard selector and ancestor options, the with method should add any other options that make sense for the particular subclass.

需要添加其他选项的测试工具应该根据需要扩展 BaseHarnessFilters 接口和其它可选属性。HarnessPredicate 为添加选项提供了一些便利方法。

Harnesses that need to add additional options should extend the BaseHarnessFilters interface and additional optional properties as needed. HarnessPredicate provides several convenience methods for adding options.

方法

Method

说明

Description

static stringMatches(s: string | Promise<string>, pattern: string | RegExp): Promise<boolean>

按照 stringRegExp 类型的模式比较字符串或字符串的 Promise,并返回一个表明它是否匹配 Promise

Compares a string or Promise of a string against a string or RegExp and returns a boolean Promise indicating whether it matches.

addOption<O>(name: string, option: O | undefined, predicate: (harness: T, option: O) => Promise<boolean>): HarnessPredicate<T>

创建一个新的 HarnessPredicate 来强制执行当前的所有条件,再加上 predicate 参数指定的新约束条件。如果 option 参数为 undefined,此 predicate 总是返回 true。

Creates a new HarnessPredicate that enforces all of the conditions of the current one, plus the new constraint specified by the predicate parameter. If the option parameter is undefined the predicate is considered to be always true.

add(description: string, predicate: (harness: T) => Promise<boolean>): HarnessPredicate<T>

创建一个新的 HarnessPredicate 来强制执行当前的所有条件,再加上 predicate 参数指定的新约束条件。

Creates a new HarnessPredicate that enforces all of the conditions of the current one, plus the new constraint specified by the predicate parameter.

例如,当使用菜单时,根据触发器文本添加一种过滤方式并根据文本来过滤菜单项会很有用:

For example, when working with a menu it would likely be useful to add a way to filter based on trigger text and to filter menu items based on their text:

interface MyMenuHarnessFilters extends BaseHarnessFilters {
  /** Filters based on the trigger text for the menu. */
  triggerText?: string | RegExp;
}

interface MyMenuItemHarnessFilters extends BaseHarnessFilters {
  /** Filters based on the text of the menu item. */
  text?: string | RegExp;
}

class MyMenuHarness extends ComponentHarness {
  static hostSelector = 'my-menu';

  /** Creates a `HarnessPredicate` used to locatr a particular `MyMenuHarness`. */
  static with(options: MyMenuHarnessFilters): HarnessPredicate<MyMenuHarness> {
    return new HarnessPredicate(MyMenuHarness, options)
        .addOption('trigger text', options.triggerText,
            (harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
  }

  protected getPopupHarness = this.locatorFor(MyPopupHarness);

  /** Gets the text of the menu trigger. */
  async getTriggerText(): Promise<string> {
    const popupHarness = await this.getPopupHarness();
    return popupHarness.getTriggerText();
  }

  ...
}

class MyMenuItemHarness extends ComponentHarness {
  static hostSelector = 'my-menu-item';

  /** Creates a `HarnessPredicate` used to locatr a particular `MyMenuItemHarness`. */
  static with(options: MyMenuItemHarnessFilters): HarnessPredicate<MyMenuItemHarness> {
    return new HarnessPredicate(MyMenuItemHarness, options)
        .addOption('text', options.text,
            (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
  }

  /** Gets the text of the menu item. */
  async getText(): Promise<string> {
    const host = await this.host();
    return host.text();
  }
}

你可以用 HarnessPredicate 代替 ComponentHarness 传给 HarnessLoaderLocatorFactoryComponentHarness 上的任何 API。这样,测试作者就可以在创建测试工具实例时轻松定位到特定的组件实例。它还能让测试工具的作者利用同样的 HarnessPredicate,在他们的测试工具类上支持更强大的 API。例如前面看过的 MyMenuHarnessgetItems 方法。现在可以很容易地扩展它以允许本测试工具的用户搜索特定的菜单项:

You can pass a HarnessPredicate in place of a ComponentHarness class to any of the APIs on HarnessLoader, LocatorFactory, or ComponentHarness. This allows test authors to easily target a particular component instance when creating a harness instance. It also allows the harness author to leverage the same HarnessPredicate to enable more powerful APIs on their harness class. For example, consider the getItems method on the MyMenuHarness shown above. This can now easily be expanded to allow users of the harness to search for particular menu items:

class MyMenuHarness extends ComponentHarness {
  static hostSelector = 'my-menu';

  /** Gets a list of items in the menu, optionally filtered based on the given criteria. */
  async getItems(filters: MyMenuItemHarnessFilters = {}): Promise<MyMenuItemHarness[]> {
    const getFilteredItems = this.locatorForAll(MyMenuItemHarness.with(filters));
    return getFilteredItems();
  }

  ...
}

有些组件会使用 <ng-content> 把其他内容投影到组件的模板中。当为这样的组件创建一个测试工具时,你可以给这个测试工具用户提供一个 <ng-content> 容器范围内的 HarnessLoader 实例。这可以让测试工具的用户能够为任何作为内容传进来的组件加载额外的测试工具。ComponentHarness 有几个 API 可以用来为这种情况创建 HarnessLoader 实例。

Some components use <ng-content> to project additional content into the component's template. When creating a harness for such a component, you can give the harness user a HarnessLoader instance scoped to the element containing the <ng-content>. This allows the user of the harness to load additional harnesses for whatever components were passed in as content. ComponentHarness has several APIs that can be used to create HarnessLoader instances for cases like this.

方法

Method

说明

Description

harnessLoaderFor(selector: string): Promise<HarnessLoader>

获取一个 Promise,它解析为以指定的选择器相匹配的第一个元素为根的 HarnessLoader;如果没有找到此组件,则 Promise 会拒绝。

Gets a Promise for a HarnessLoader rooted at the first element matching the given selector, if no element is found the Promise rejects.

harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>

获取一个 Promise,它解析为与指定选择器匹配的第一个元素,如果没有找到此元素,则 Promise 会解析为 null

Gets a Promise for a HarnessLoader rooted at the first element matching the given selector, if no element is found the Promise resolves to null.

harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>

获取一个 Promise,它是一个以和指定选择器匹配的每个元素为根的 HarnessLoader 列表

Gets a Promise for a list of HarnessLoader, one rooted at each element matching the given selector.

前面讨论过的 MyPopup 组件就是一个很好的例子,它包含了一些用户可能希望为其加载测试工具的内容。 MyPopupHarness 可以通过扩展 ContentContainerComponentHarness 来为此添加支持。

The MyPopup component discussed earlier is a good example of a component with arbitrary content that users may want to load harnesses for. MyPopupHarness could add support for this by extending ContentContainerComponentHarness.

class MyPopupHarness extends ContentContainerComponentHarness<string> {
  static hostSelector = 'my-popup';
}

有时组件工具可能需要访问相应组件的宿主元素之外的元素。那些使用 CDK 浮层的组件就是这样的例子。 组件的宿主元素外面的 CDK 浮层会创建一个直接附着在 body 上的元素。在这个例子中,ComponentHarness 提供了一个方法,可以用来获取根元素的 LocatorFactory。 此 LocatorFactory 支持大多数与 ComponentHarness 基类相同的 API,然后可以用来相对于该文档的根元素进行查询。

There are times when a component harness might need to access elements outside of its corresponding component's host element. Components that use CDK overlay serve as examples of this. The CDK overlay creates an element that is attached directly to the body, outside of the component's host element. In this case, ComponentHarness provides a method that can be used to get a LocatorFactory for the root element of the document. The LocatorFactory supports most of the same APIs as the ComponentHarness base class, and can then be used to query relative to the document's root element.

方法

Method

说明

Description

documentRootLocatorFactory(): LocatorFactory

创建一个以该文档的根元素为根的 LocatorFactory

Creates a LocatorFactory rooted at the document's root element.

假设 MyPopup 组件使用了 CDK 浮层作为弹出内容,而不是它自己模板中的一个元素。在这种情况下,MyPopupHarness 可能必须通过 documentRootLocatorFactory() 访问其内容元素:

Consider if the MyPopup component above used the CDK overlay for the popup content, rather than an element in its own template. In this case, MyPopupHarness would have to access the content element via documentRootLocatorFactory():

class MyPopupHarness extends ComponentHarness {
  static hostSelector = 'my-popup';

  /** Gets a `HarnessLoader` whose root element is the popup's content element. */
  async getHarnessLoaderForContent(): Promise<HarnessLoader> {
    const rootLocator = this.documentRootLocatorFactory();
    return rootLocator.harnessLoaderFor('my-popup-content');
  }
}

TestElement 上的方法会自动触发 Angular 的变更检测,并等待 NgZone 中的任务,所以在大多数情况下,这些方法不需要特别的工作来让线程作者等待异步任务。 然而,在一些边缘情况下这可能还不够。

The methods on TestElement automatically trigger Angular's change detection and wait for tasks inside the NgZone, so in most cases no special effort is required for harness authors to wait on asynchronous tasks. However, there are some edge cases where this may not be sufficient.

在某些情况下,在完全刷新动画事件之前,Angular 动画可能还需要第二个变更检测循环和等待 NgZone 进入稳定状态。 如果需要这样做,ComponentHarness 提供了一个 forceStabilize() 方法,可以调用它来进行第二轮测试。

Under some circumstances, Angular animations may require a second cycle of change detection and subsequent NgZone stabilization before animation events are fully flushed. In cases where this is needed, the ComponentHarness offers a forceStabilize() method that can be called to do the second round.

另外,某些组件可能会故意在 NgZone 之外安排任务,这通常是使用 NgZone.runOutsideAngular 完成的。 在这种情况下,对应的测试工具可能需要显式等待 NgZone 以外的任务,因为这不会自动发生。 ComponentHarness 为此提供了一个名为 waitForTasksOutsideAngular 的方法。

Additionally, some components may intentionally schedule tasks outside of NgZone, this is typically accomplished by using NgZone.runOutsideAngular. In this case, the corresponding harness may need to explicitly wait for tasks outside NgZone, as this does not happen automatically. ComponentHarness offers a method called waitForTasksOutsideAngular for this purpose.

方法

Method

说明

Description

forceStabilize(): Promise<void>

在 Angular 中显式运行一轮变更检测,并等待 NgZone 稳定下来。

Explicitly runs a round of change detection in Angular and waits for NgZone to stabilize.

waitForTasksOutsideAngular(): Promise<void>

等待 NgZone 以外的任务完成。

Waits for tasks scheduled outside of NgZone to complete.

测试工具环境作者是那些希望在其他测试环境中增加使用组件测试工具支持的开发人员。开箱即用的 Angular CDK 组件测试工具可用于 Protractor E2E 测试和 Karma 单元测试。开发人员可以创建 TestElementHarnessEnvironment 的自定义实现来支持更多环境。

Harness environment authors are developers who want to add support for using component harnesses in additional testing environments. Out-of-the-box, Angular CDK's component harnesses can be used in Protractor E2E tests and Karma unit tests. Developers can support additional environments by creating custom implementations of TestElement and HarnessEnvironment.

添加对新测试环境的支持的第一步是创建一个 TestElement 实现。TestElement 接口用作 DOM 元素的环境无关表示形式。它能让测试工具与 DOM 元素进行交互,而不用管底层环境如何。由于某些环境不支持与 DOM 元素的同步交互(比如 webdriver),因此所有的 TestElement 方法都是异步的,返回一个包含该操作结果的 Promise

The first step in adding support for a new testing environment is to create a TestElement implementation. The TestElement interface serves as an environment-agnostic representation of a DOM element; it lets harnesses interact with DOM elements regardless of the underlying environment. Because some environments don't support interacting with DOM elements synchronously (e.g. webdriver), all of the TestElement methods are asynchronous, returning a Promise with the result of the operation.

方法

Method

说明

Description

blur(): Promise<void>

让此元素失焦。

Blurs the element.

clear(): Promise<void>

从此元素中清除文本(仅适用于 <input><textarea> )。

Clears the text from an element (only applies for <input> and <textarea>).

click(relativeX?: number, relativeY?: number): Promise<void>

在相对于它左上角的坐标点击此元素。

Clicks an element at a point relative to it's top-left corner.

focus(): Promise<void>

让此元素获得焦点。

Focuses the element.

getCssValue(property: string): Promise<string>

获取此元素指定属性的计算 CSS 值。

Gets the computed CSS value of the given property for the element.

hover(): Promise<void>

将鼠标悬停在此元素上方。

Hovers the mouse over the element.

sendKeys(...keys: (string | TestKey)[]): Promise<void>

向此元素发送一系列按键事件。

Sends a sequence of key events to the element.

sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>

把一系列按键事件发送给此元素,同时按住一组修饰键。

Sends a sequence of key events to the element, while holding a set of modifier keys.

text(): Promise<string>

获取此元素的文本内容。

Gets the text content of the element.

getAttribute(name: string): Promise<string | null>

从此元素获取指定 HTML 属性的值。

Gets the value of the given HTML attribute for the element.

hasClass(name: string): Promise<boolean>

检查此元素是否具有指定的类。

Checks whether the element has the given class.

getDimensions(): Promise<ElementDimensions>

获取此元素的尺寸。

Gets the dimensions of the element.

getProperty(name: string): Promise<any>

从此元素获取指定属性的值。

Gets the value of the given property for the element.

matchesSelector(selector: string): Promise<boolean>

检查指定的选择器是否与此元素匹配。

Checks whether the given selector matches the element.

setInputValue(value: string): Promise<void>;

设置输入框的值。

Sets the value of a property of an input.

selectOptions(...optionIndexes: number[]): Promise<void>;

选择原生 select 元素中指定索引处的候选项。

Selects the options at the specified indexes inside of a native select element.

dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;

派发具有特定名称的事件。

Dispatches an event with a particular name.

TestElement 接口包含类似 HTMLElement 方法的大部分方法。在大多数测试环境中都存在类似的方法,这使得实现这些方法相当简单。使用 sendKeys 方法时要注意的一个重要区别是,TestKey 枚举中的键码可能与测试环境中使用的键码有所不同。环境作者应该坚持从 TestKey 代码映射到特定测试环境中的代码。

The TestElement interface consists largely of methods that resemble methods available on HTMLElement; similar methods exist in most test environments, which makes implementing the methods fairly straightforward. However, one important difference to note when implementing the sendKeys method, is that the key codes in the TestKey enum likely differ from the key codes used in the test environment. Environment authors should maintain a mapping from TestKey codes to the codes used in the particular testing environment.

Angular CDK 中的 UnitTestElementProtractorElement 实现就是实现这个接口的好例子。

The UnitTestElement and ProtractorElement implementations in Angular CDK serve as good examples of implementations of this interface.

测试作者使用 HarnessEnvironemnt 来创建用于测试的组件工具实例。

Test authors use HarnessEnvironemnt to create component harness instances for use in tests.

HarnessEnvironment 是一个抽象类,必须进行扩展才能为新环境创建一个具体的子类。在支持新的测试环境时,你必须创建一个 HarnessEnvironment 子类,为所有抽象成员添加具体的实现。

HarnessEnvironment is an abstract class that must be extended to create a concrete subclass for the new environment. When supporting a new test environment, you must create a HarnessEnvironment subclass that adds concrete implementations for all abstract members.

你会发现 HarnessEnvironment 有一个泛型类型参数:HarnessEnvironment<E>。这个参数 E 就表示环境的原始元素类型。例如,这个参数在单元测试环境下是 Element

You will notice that HarnessEnvironment has a generic type parameter: HarnessEnvironment<E>. This parameter, E, represents the raw element type of the environment. For example, this parameter is Element for unit test environments.

下面是必须要实现的抽象方法:

The following are the abstract methods that must be implemented:

方法

Method

说明

Description

abstract getDocumentRoot(): E

获取此环境的根元素(例如 document.body )。

Gets the root element for the environment (e.g. document.body).

abstract createTestElement(element: E): TestElement

为指定的原始元素创建一个 TestElement

Creates a TestElement for the given raw element.

abstract createEnvironment(element: E): HarnessEnvironment

根据指定的原始元素创建一个 HarnessEnvironment

Creates a HarnessEnvironment rooted at the given raw element.

abstract getAllRawElements(selector: string): Promise<E[]>

获取环境根元素下的所有与指定选择器匹配的原始元素。

Gets all of the raw elements under the root element of the environment matching the given selector.

abstract forceStabilize(): Promise<void>

获取一个当 NgZone 稳定时解析的 Promise。另外,只要可能,就告诉 NgZone 变稳定(比如在 fakeAsync 测试中的 flush()

Gets a Promise that resolves when the NgZone is stable. Additionally, if applicable, tells NgZone to stabilize (e.g. calling flush() in a fakeAsync test).

abstract waitForTasksOutsideAngular(): Promise<void>

获取一个 Promise,它会在 NgZone 的父区域稳定时解析。

Gets a Promise that resolves when the parent zone of NgZone is stable.

除了实现所缺的方法之外,这个类还应该为测试作者提供一种获取 ComponentHarness 实例的方法。 推荐的方法是具有一个受保护的构造函数,并提供一个名为 loader 的静态方法来返回一个 HarnessLoader 的实例。这让测试作者可以编写如下代码:SomeHarnessEnvironment.loader().getHarness(...)。根据特定环境的需要,该类可以提供几种不同的静态方法,或者要求传递参数。(例如,TestbedHarnessEnvironmentloader 方法接受了一个 ComponentFixture,该类还提供了另外一些名为 documentRootLoaderharnessForFixture 静态方法)。

In addition to implementing the missing methods, this class should provide a way for test authors to get ComponentHarness instances. The recommended approach is to have a protected constructor and provide a static method called loader that returns a HarnessLoader instance. This allows test authors to write code like: SomeHarnessEnvironment.loader().getHarness(...). Depending on the needs of the particular environment, the class may provide several different static methods or require arguments to be passed. (e.g. the loader method on TestbedHarnessEnvironment takes a ComponentFixture, and the class provides additional static methods called documentRootLoader and harnessForFixture).

Angular CDK 中的 TestbedHarnessEnvironmentProtractorHarnessEnvironment 的实现就是很好的例子。

The TestbedHarnessEnvironment and ProtractorHarnessEnvironment implementations in Angular CDK serve as good examples of implementations of this interface.

为了支持 manualChangeDetectionparallel API,你的环境应该为自动变更检测状态安装一个处理器。

In order to support the manualChangeDetection and parallel APIs, your environment should install a handler for the auto change detection status.

当你的环境想要开始处理自动变更检测的状态时,可以调用 handleAutoChangeDetectionStatus(handler)。此处理器函数会要求一个 AutoChangeDetectionStatus 参数,其中有两个属性:

When your environment wants to start handling the auto change detection status it can call handleAutoChangeDetectionStatus(handler). The handler function will receive a AutoChangeDetectionStatus which has two properties:

  • isDisabled: boolean - 表示当前是否禁用了自动检测。当为 true 时,你的环境的 forceStabilize 方法应该什么也不做。这样,用户就可以手动触发变更检测。

    isDisabled: boolean - Indicates whether auto change detection is currently disabled. When true, your environment's forceStabilize method should act as a no-op. This allows users to trigger change detection manually instead.

  • onDetectChangesNow?: () => void - 如果指定了这个可选的回调函数,你的环境应立即触发变更检测,并在变更检测完成时调用该回调函数。

    onDetectChangesNow?: () => void - If this optional callback is specified, your environment should trigger change detection immediately and call the callback when change detection finishes.

如果你的环境要停止处理自动变更检测的状态,可以调用 stopHandlingAutoChangeDetectionStatus()

If your environment wants to stop handling auto change detection status it can call stopHandlingAutoChangeDetectionStatus().