In an Angular CDK ComponentHarness, how do locatorFor, locatorForOptional and locatorForAll differ, and why do they return functions rather than elements?
answer
- one, maybe one, or many
- reject, null, or empty array
- the search happens at call time
- templates recreate DOM nodes
basics
~20 slocatorFor resolves to the first match and rejects if none, locatorForOptional resolves to the first match or null, locatorForAll resolves to an array; they return functions so each call queries the current DOM, never a stale element.
solid answer
~40 sAll three are factories on `ComponentHarness` that return an async function; awaiting that function searches under the harness's host. `locatorFor` resolves to the first match and rejects when there is none, `locatorForOptional` resolves to the first match or `null`, and `locatorForAll` resolves to every match in DOM order, or an empty array. Queries can be CSS selectors, which yield `TestElement`s, or harness classes and `HarnessPredicate`s, which yield sub-harnesses. They return functions because Angular templates destroy and recreate DOM, for example an `@if` panel that closes and reopens, so a reference captured when the harness was built would go stale. Declaring the locator once and calling it per use always hits the current DOM.
code
ts · 20 linesimport { ComponentHarness } from '@angular/cdk/testing';
export class DropdownHarness extends ComponentHarness {
static hostSelector = 'app-dropdown';
// Required: a missing trigger is a real failure, so the locator rejects.
protected trigger = this.locatorFor('button.dropdown-trigger');
// Optional: a closed dropdown has no panel, so the locator resolves to null.
protected panel = this.locatorForOptional('[role="listbox"]');
// Repeated: zero options is a valid state, so the locator resolves to [].
protected optionElements = this.locatorForAll('[role="option"]');
async isOpen(): Promise<boolean> {
return (await this.panel()) !== null;
}
async countOptions(): Promise<number> {
return (await this.optionElements()).length;
}
}go deeper
Recall the three names and their miss behaviour: reject, null, empty array. Remember to await the returned function before using the element.
Explain why locators are lazy functions, how @if and @for recreate DOM, and how mixing CSS selectors with harness classes changes the result type.
Choose the variant that encodes the component's contract, required versus optional versus repeated, so harness failures point at real regressions.
Set harness-authoring conventions for a component library, locator visibility, naming and miss semantics, so dozens of harnesses behave predictably for consumers.
## Three locator factories Inside a **`ComponentHarness`** (from `@angular/cdk/testing`), elements and sub-harnesses are found with three factory methods inherited from the base class. They share one important trait: each **returns a function**, and calling that function (with `await`) performs the search. | Factory | The returned function resolves to | When nothing matches | |---|---|---| | `locatorFor(...queries)` | the **first** match | the promise **rejects** | | `locatorForOptional(...queries)` | the first match | resolves to **`null`** | | `locatorForAll(...queries)` | **every** match, in DOM order | resolves to **`[]`** | The search runs **under the harness's host element**. A query can be: - a **CSS selector** string, which yields a `TestElement`; - a **harness class**, which yields an instance of that harness for each element matching its `hostSelector`; - a **`HarnessPredicate`**, which yields harnesses that also pass the predicate's filters. Several queries can be passed together. For `locatorFor`, the result is the first element in DOM order that matches **any** query; if one element matches several queries, the query listed first decides whether you get a harness or a `TestElement`. `locatorForAll` returns, for each matching element in DOM order, one `TestElement` if any selector matches it (never duplicated) plus one instance of each harness class that matches it, so `locatorForAll(OptionHarness, '[role="option"]')` returns both a harness and a `TestElement` for each option. ## Why functions and not elements A harness is constructed once and used across many interactions. If a locator returned an element directly, the harness would keep a reference to the element that existed **at construction time**. Angular templates routinely **destroy and recreate** DOM: an `@if` block that closes and reopens a dropdown panel produces a **new** list element each time, and an `@for` block recreates rows whose tracking key changed. A cached reference would then point at a detached node, and the test would click something no user can see. Returning a function means every call **queries the current DOM**. The harness declares *how* to find the trigger once, as a field, and each method asks for the element *when it needs it*. ## Choosing between them in a dropdown harness 1. **`locatorFor`** for elements whose absence is a bug: the dropdown's trigger button. A rejection gives the test a clear failure at the point of use. 2. **`locatorForOptional`** for elements whose absence is a valid state: the option panel of a closed dropdown. `isOpen()` becomes "the optional panel is not `null`". 3. **`locatorForAll`** for repeated elements where zero is meaningful: the options, or the chips of a multi-select. An empty array is an answer, not an error. Using `locatorFor` for an optional element turns a legitimate state into a failure; using `locatorForOptional` for a required one hides a real regression behind a `null` check. ## Related tools on the same base class - **`host()`** resolves to the `TestElement` of the host element itself, for reading host attributes or classes. - **`documentRootLocatorFactory()`** returns a `LocatorFactory` rooted at the document, with the same `locatorFor` family, for content rendered outside the host such as overlays. - A **`LocatorFactory`**, such as the one `documentRootLocatorFactory()` returns, also offers `harnessLoaderFor(selector)`, `harnessLoaderForOptional` and `harnessLoaderForAll`, which return `HarnessLoader`s scoped to an element. For projected content, a harness can extend `ContentContainerComponentHarness`, which exposes loader methods over its content. ## Locating sub-harnesses Passing a harness class instead of a selector composes harnesses. A dropdown built from an `OptionHarness` per row can declare `protected options = this.locatorForAll(OptionHarness)`, and each result is a harness with its own methods (`getText()`, `isSelected()`), not a raw element. Passing `OptionHarness.with({ text: 'Chile' })` narrows the result with a `HarnessPredicate`. The miss semantics stay the same: `locatorFor(OptionHarness)` rejects, the optional variant resolves to `null`, the all variant to `[]`. This is how large components keep their harnesses small: each layer knows only its own markup and delegates to the harness of the component it contains. ## Common mistakes - Treating the locator as the element: `this.trigger.click()` instead of `(await this.trigger()).click()`. - Calling the locator once in the constructor and storing the result, which reintroduces the stale-element problem. - Expecting `locatorFor` to resolve to `null` on a miss, and writing `if (!trigger)` checks that never run because the promise already rejected. - Using `locatorForAll(...)` and then indexing `[0]` where `locatorFor` states the intent more clearly.
- What does locatorFor(OptionHarness, '[role="option"]') return when the first option element matches both queries?An `OptionHarness`. `locatorFor` returns the first element in DOM order that matches any query, and when one element matches several queries the one listed first decides the result type. Swapping the arguments to `locatorFor('[role="option"]', OptionHarness)` returns a `TestElement` for the same element.
- How do you locate an option panel that the dropdown renders in an overlay outside its host?Locators created directly on the harness search under its host element, so they cannot see content attached to `document.body`. Use `this.documentRootLocatorFactory()`, which returns a `LocatorFactory` rooted at the document with the same `locatorFor`, `locatorForOptional` and `locatorForAll` methods.
saying these in an interview costs you the question
- locatorFor resolves to null when nothing matches
- Locators return elements, so you can call click() on them directly
- Calling a locator once in the constructor is a fine optimisation
- locatorForAll rejects when it finds no elements
- Locators search the whole document by default