In Angular CDK testing, how do you pick one dropdown among several on a page using HarnessLoader queries and a HarnessPredicate?
answer
- first match wins silently
- selector and ancestor come free
- a static factory for filters
- undefined options are ignored
basics
~10 sPass a HarnessPredicate instead of the bare harness class: DropdownHarness.with({ selector: '[name="country"]' }) or an ancestor filter narrows the matches, because getHarness otherwise returns whichever matching dropdown comes first.
solid answer
~30 s`HarnessLoader.getHarness` returns the first matching instance in DOM order and throws only when there is none, so with three dropdowns it silently picks the first. To target one, I pass a `HarnessPredicate`: `BaseHarnessFilters` gives every harness `selector`, matched against the host element, and `ancestor`, matched against a containing element. Harness authors add a static `with()` that builds `new HarnessPredicate(DropdownHarness, options)` and chains `addOption` for extra filters such as `label`; `addOption` skips undefined values, and `HarnessPredicate.stringMatches` treats a string as exact and a `RegExp` as partial. The loader also offers `getHarnessOrNull`, `getAllHarnesses`, `getHarnessAtIndex`, `countHarnesses` and `hasHarness` for other cardinalities.
code
ts · 17 linesimport { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { DropdownHarness } from './dropdown-harness';
import { ProfileForm } from './profile-form';
it('picks the country dropdown out of three', async () => {
const fixture = TestBed.createComponent(ProfileForm);
const loader = TestbedHarnessEnvironment.loader(fixture);
expect(await loader.countHarnesses(DropdownHarness)).toBe(3);
const country = await loader.getHarness(DropdownHarness.with({ selector: '[name="country"]' }));
await country.selectOption('Chile');
expect(await loader.hasHarness(DropdownHarness.with({ label: 'Chile' }))).toBe(true);
expect(await loader.getHarnessOrNull(DropdownHarness.with({ label: /Mars/ }))).toBeNull();
});go deeper
Recall that getHarness returns the first match and that DropdownHarness.with({ selector }) narrows which instance you get.
Explain the loader's query methods and their miss behaviour, the selector and ancestor base filters, and how with() builds a HarnessPredicate with addOption.
Pick filters that express intent and survive layout changes, and design with() options for your own harnesses so consumers never need indexes.
Standardise filter options across a component library's harnesses so teams query every widget the same way, and treat index-based lookups as a review smell.
## The situation A profile form renders **three** `<app-dropdown>` components: country, language and time zone. A test wants the country one. Asking a `HarnessLoader` for `DropdownHarness` alone is ambiguous, so the CDK offers two tools: the **loader's query methods**, which decide how many results you get and what happens on a miss, and **`HarnessPredicate`**, which decides which instances qualify. ## HarnessLoader query methods All of them live on `HarnessLoader` (from `@angular/cdk/testing`) and search **under the loader's root element**. Each takes a **harness query**: a harness class or a `HarnessPredicate`. | Method | Resolves to | On no match | |---|---|---| | `getHarness(query)` | the **first** match in DOM order | **throws** | | `getHarnessOrNull(query)` | the first match | resolves to `null` | | `getHarnessAtIndex(query, index)` | the match at that index | throws | | `getAllHarnesses(query)` | every match | resolves to `[]` | | `countHarnesses(query)` | the number of matches | `0` | | `hasHarness(query)` | whether any match exists | `false` | Note what `getHarness` does with **several** matches: it returns the **first**, silently. On a page with three dropdowns, `loader.getHarness(DropdownHarness)` hands you whichever is first in the DOM, and a test written that way breaks as soon as someone reorders the form. That is the reason to filter. ## Filtering with HarnessPredicate A **`HarnessPredicate<T>`** pairs a harness class with conditions. You can build one directly: - `new HarnessPredicate(DropdownHarness, { selector: '[name="country"]' })` keeps dropdowns whose **host element** matches the extra selector. - `new HarnessPredicate(DropdownHarness, { ancestor: '.billing-section' })` keeps dropdowns **nested under** an element matching that selector. Those two options come from **`BaseHarnessFilters`**, which every harness supports. For anything else, harness authors add a static **`with()`** method: 1. Declare a filters interface extending `BaseHarnessFilters`, for example `{ label?: string | RegExp }`. 2. In `with(options)`, create `new HarnessPredicate(DropdownHarness, options)`. 3. Chain `addOption('label', options.label, (harness, label) => HarnessPredicate.stringMatches(harness.getSelectedLabel(), label))`. Two details make this work: - **`addOption` ignores the predicate when the option value is `undefined`**, so `DropdownHarness.with({})` matches every dropdown and callers pass only the filters they care about. - **`HarnessPredicate.stringMatches`** treats a **string** pattern as an **exact** match and a **`RegExp`** as a **partial** match, so `with({ label: /Norw/ })` matches "Norway" while `with({ label: 'Norw' })` does not. `add(description, predicate)` adds an unconditional async check when a filter is not tied to an option value. ## Putting it together - `await loader.getHarness(DropdownHarness.with({ selector: '[name="country"]' }))` picks the country dropdown by its host attribute. - `await loader.getAllHarnesses(DropdownHarness.with({ ancestor: '.billing-section' }))` gets every dropdown in one section. - `await loader.hasHarness(DropdownHarness.with({ label: 'Select a time zone' }))` asserts a placeholder is still showing. The same predicates work anywhere a harness query is accepted, including inside another harness: `this.locatorForAll(OptionHarness.with({ text: 'Chile' }))`. ## Other ways to scope a search Predicates are not the only way to narrow a query. The loader itself can be re-rooted: - `loader.getChildLoader('.billing-section')` returns a new `HarnessLoader` rooted at the **first** element matching the selector, and **throws** if none matches; `getAllChildLoaders(selector)` returns one loader per match. - A `LocatorFactory`, such as the one a harness gets from `documentRootLocatorFactory()`, offers `harnessLoaderFor(selector)`, `harnessLoaderForOptional` and `harnessLoaderForAll` for the same purpose from its own root. - A harness for a container component can extend `ContentContainerComponentHarness`, which exposes loader methods over its projected content. Re-rooting and filtering combine: `(await loader.getChildLoader('.billing-section')).getHarness(DropdownHarness.with({ label: /Select/ }))` reads as "the unselected dropdown in the billing section". ## Choosing the filter - Prefer a **stable attribute the consumer controls**, such as a `name` or form-control binding on the host, via `selector`. - Use **`ancestor`** when the page is structured into sections a user would recognise. - Use **component-specific filters** like `label` when the visible state is what identifies the instance. - Avoid **index-based** access (`getHarnessAtIndex`) unless order itself is what the test asserts.
- Why is getHarness(DropdownHarness) on a page with three dropdowns a latent bug even when the test passes today?`getHarness` returns the first match in DOM order without complaining about the others. The test is implicitly bound to the form's layout: move the country dropdown below the language one and it silently drives the wrong control. A predicate that names the intended instance makes the dependency explicit and the failure clear.
- What does DropdownHarness.with({ label: 'Norw' }) match if a dropdown shows 'Norway'?Nothing, assuming the harness implements the label filter with `HarnessPredicate.stringMatches`. A string pattern must match exactly; only a `RegExp` such as `/Norw/` allows a partial match. That distinction is easy to miss and is why `with()` filters usually type the option as `string | RegExp`.
saying these in an interview costs you the question
- getHarness throws when more than one instance matches
- Filtering by selector matches any element inside the component
- An undefined filter option matches no harnesses
- stringMatches with a string pattern allows substring matches
- getHarnessAtIndex is the preferred way to choose among instances