From cf0270c0c0feea70a55faf55ac09da4179e35766 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 22 Oct 2019 13:55:30 -0700 Subject: [PATCH 1/3] docs(cdk/testing): Add documentation for harness authors --- src/cdk/testing/testing.md | 393 ++++++++++++++++++++++++++++++++++++- 1 file changed, 383 insertions(+), 10 deletions(-) diff --git a/src/cdk/testing/testing.md b/src/cdk/testing/testing.md index bde9d78f4fec..860b5678d497 100644 --- a/src/cdk/testing/testing.md +++ b/src/cdk/testing/testing.md @@ -36,15 +36,15 @@ Test authors are developers using component harnesses written by someone else to 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` +#### Working with `ComponentHarness` classes -This is the abstract base class for all component harnesses. Every harness extends -`ComponentHarness`. All `ComponentHarness` subclasses have a static property, `hostSelector`, that +`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. -#### `TestbedHarnessEnvironment` and `ProtractorHarnessEnvironment` +#### Using `TestbedHarnessEnvironment` and `ProtractorHarnessEnvironment` 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 @@ -117,7 +117,7 @@ Since Protractor does not deal with fixtures, the API in this environment is sim `HarnessLoader` returned by the `loader()` method should be sufficient for loading all necessary `ComponentHarness` instances. -#### `HarnessLoader` +#### Creating harnesses with `HarnessLoader` 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. @@ -134,8 +134,8 @@ are used to create `ComponentHarness` instances for elements under this root ele 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`](#harnesspredicate) are discussed in the -[API for component harness authors](#api-for-component-harness-authors); harness authors should +[details of `HarnessPredicate`](#filtering-harness-instances-with-harnesspredicate) are discussed in +the [API for component harness authors](#api-for-component-harness-authors); harness authors should provide convenience methods on their `ComponentHarness` subclass to facilitate creation of `HarnessPredicate` instances. However, if the harness author's API is not sufficient, they can be created manually. @@ -169,11 +169,384 @@ it('reads properties in parallel', async () => { ### API for component harness authors -TODO(mmalerba): Fill in docs for harness authors +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. -#### `HarnessPredicate` +#### Extending `ComponentHarness` -TODO(mmalerba): Fill in docs for `HarnessPredicate` +The abstract `ComponentHarness` class is the base class for all component harnesses. To work with +the CDK's component harness infrastructure, a class must extend `ComponentHarness` and implement a +static property `hostSelector`. The `hostSelector` property is used to identify elements in the DOM +that match this harness subclass. In most cases the `hostSelector` should be the same as the +`selector` of the `Component` or `Directive` that the harness corresponds to. For example, consider +a simple popup component with a helper directive to manage some accessibility attributes: + +```ts +@Component({ + selector: 'my-popup', + template: ` + +
+ ` +}) +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: + +```ts +class MyPopupHarness extends ComponentHarness { + static hostSelector = 'my-popup'; +} +``` + +Other than the `hostSelector` property, there are no other properties or methods that are required +to implement on a `ComponentHarness` subclass, though it is highly recommended to add a static +`with` method that can be used to generate `HarnessPredicate` instances. This is discussed in more +detail in the [`HarnessPredicate`](#filtering-harness-instances-with-harnesspredicate) section +below. + +#### Finding elements in the component's DOM + +Each instance of the `ComponentHarness` subclass represents a particular instance of the +corresponding component. The corresponding component instance's host element can be accessed via the +`host` method on the `ComponentHarness` base class. + +In addition to being able to access the component's host element, `ComponentHarness` has several +methods that can be used to locate elements under the host element; that is, elements that are part +of the component's template or content. These methods are `locatorFor`, `locatorForOptional`, and +`locatorForAll`. One important caveat to understand about these methods is that they do not find +elements, they _create functions_ that can be used to find elements. This avoids caching references +to elements that later turn stale (for example when an `ngIf` binding updates). + +| Method | Description | +| ------ | ----------- | +| `host(): Promise` | Returns a `Promise` for the host element of the corresponding component instance. | +| `locatorFor(selector: string): () => 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` | 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` | Creates a function that returns a `Promise` for a list of all elements matching the given selector when called. | + +For example, the `MyPopupHarness` class discussed above could provide methods to get the trigger +and content elements as follows: + +```ts +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'); +} +``` + +#### Working with `TestElement` instances + +The various methods described above for finding elements all return a `TestElement` class. +`TestElement` offers a number of methods to interact with the underlying DOM: + +| Method | Description | +| ------ | ----------- | +| `blur(): Promise` | Blurs the element. | +| `clear(): Promise` | Clears the text in the element (intended for `` and `