The Angular CDK Testing module provides a comprehensive component harness system for creating reliable, maintainable component tests. It abstracts away implementation details and provides a consistent API for interacting with components across different test environments.
Base class for creating component test harnesses that provide a high-level API for interacting with components.
/**
* Base class for component harnesses
*/
abstract class ComponentHarness {
/**
* Get the host element for this harness
* @returns Promise resolving to the host TestElement
*/
host(): Promise<TestElement>;
/**
* Create a locator for a single sub-component
* @param selector - Harness constructor or predicate
* @returns Function that locates the component
*/
locatorFor<T extends ComponentHarness>(
selector: ComponentHarnessConstructor<T> | HarnessPredicate<T>
): AsyncFactoryFn<T>;
/**
* Create a locator for an optional sub-component
* @param selector - Harness constructor or predicate
* @returns Function that locates the component or returns null
*/
locatorForOptional<T extends ComponentHarness>(
selector: ComponentHarnessConstructor<T> | HarnessPredicate<T>
): AsyncFactoryFn<T | null>;
/**
* Create a locator for all matching sub-components
* @param selector - Harness constructor or predicate
* @returns Function that locates all matching components
*/
locatorForAll<T extends ComponentHarness>(
selector: ComponentHarnessConstructor<T> | HarnessPredicate<T>
): AsyncFactoryFn<T[]>;
/**
* Wait for asynchronous tasks outside Angular to complete
* @returns Promise that resolves when tasks are complete
*/
waitForTasksOutsideAngular(): Promise<void>;
/**
* Force Angular change detection and wait for async tasks
* @returns Promise that resolves when stabilized
*/
forceStabilize(): Promise<void>;
/**
* Create a child harness environment
* @param selector - CSS selector for child element
* @returns Promise of child harness environment
*/
protected childEnvironment(selector: string): Promise<HarnessEnvironment<Element>>;
/**
* Get all child elements matching a selector
* @param selector - CSS selector
* @returns Promise of TestElement array
*/
protected getAllChildElements(selector: string): Promise<TestElement[]>;
}
/**
* Constructor type for component harnesses
*/
interface ComponentHarnessConstructor<T extends ComponentHarness> {
new (locator: LocatorFnResult<T>): T;
hostSelector: string;
}
/**
* Function type for async factory functions
*/
interface AsyncFactoryFn<T> {
(): Promise<T>;
}Interface representing an element in tests with methods for interaction.
/**
* Interface for elements in component tests
*/
interface TestElement {
/**
* Blur the element
* @returns Promise that resolves when blur is complete
*/
blur(): Promise<void>;
/**
* Clear the element's value (for input elements)
* @returns Promise that resolves when clear is complete
*/
clear(): Promise<void>;
/**
* Click the element
* @param relativeX - X coordinate relative to element
* @param relativeY - Y coordinate relative to element
* @returns Promise that resolves when click is complete
*/
click(relativeX?: number, relativeY?: number): Promise<void>;
/**
* Focus the element
* @returns Promise that resolves when focus is complete
*/
focus(): Promise<void>;
/**
* Get a CSS property value
* @param property - CSS property name
* @returns Promise resolving to property value
*/
getCssValue(property: string): Promise<string>;
/**
* Hover over the element
* @returns Promise that resolves when hover is complete
*/
hover(): Promise<void>;
/**
* Move mouse away from the element
* @returns Promise that resolves when mouse move is complete
*/
mouseAway(): Promise<void>;
/**
* Send keystrokes to the element
* @param keys - Keys to send (can include special keys)
* @returns Promise that resolves when keys are sent
*/
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
/**
* Get the element's text content
* @returns Promise resolving to text content
*/
text(): Promise<string>;
/**
* Get an attribute value
* @param name - Attribute name
* @returns Promise resolving to attribute value or null
*/
getAttribute(name: string): Promise<string | null>;
/**
* Check if element has a CSS class
* @param name - Class name
* @returns Promise resolving to true if class exists
*/
hasClass(name: string): Promise<boolean>;
/**
* Get element dimensions and position
* @returns Promise resolving to element dimensions
*/
getDimensions(): Promise<ElementDimensions>;
/**
* Get a DOM property value
* @param name - Property name
* @returns Promise resolving to property value
*/
getProperty(name: string): Promise<any>;
/**
* Set the value of an input element
* @param value - Value to set
* @returns Promise that resolves when value is set
*/
setInputValue(value: string): Promise<void>;
/**
* Select options in a select element
* @param optionIndexes - Indexes of options to select
* @returns Promise that resolves when options are selected
*/
selectOptions(...optionIndexes: number[]): Promise<void>;
/**
* Check if element matches a CSS selector
* @param selector - CSS selector
* @returns Promise resolving to true if matches
*/
matchesSelector(selector: string): Promise<boolean>;
/**
* Check if element is focused
* @returns Promise resolving to true if focused
*/
isFocused(): Promise<boolean>;
/**
* Dispatch a custom event
* @param name - Event name
* @param data - Event data
* @returns Promise that resolves when event is dispatched
*/
dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void>;
}
/**
* Element dimensions and position information
*/
interface ElementDimensions {
top: number;
left: number;
width: number;
height: number;
}
/**
* Event data type for custom events
*/
type EventData = string | number | boolean | undefined | null | EventData[] | {[key: string]: EventData};Abstract base class for harness execution environments.
/**
* Base class for harness execution environments
* @template E Element type for the environment
*/
abstract class HarnessEnvironment<E> {
/**
* Create a TestElement from a raw element
* @param element - Raw element
* @returns TestElement wrapper
*/
protected abstract createTestElement(element: E): TestElement;
/**
* Create a child environment for an element
* @param element - Element for child environment
* @returns Child harness environment
*/
protected abstract createEnvironment(element: E): HarnessEnvironment<E>;
/**
* Get all raw elements matching a selector
* @param selector - CSS selector
* @returns Promise of raw elements
*/
protected abstract getAllRawElements(selector: string): Promise<E[]>;
/**
* Get the current task state
* @returns Promise of task state information
*/
protected abstract requestTaskState(): Promise<TaskState>;
/**
* Wait for asynchronous tasks outside Angular
* @returns Promise that resolves when tasks complete
*/
protected abstract waitForTasksOutsideAngular(): Promise<void>;
}
/**
* Task state information
*/
interface TaskState {
/** Whether there are pending microtasks */
hasPendingMicrotasks: boolean;
/** Whether there are pending macrotasks */
hasPendingMacrotasks: boolean;
/** Whether there are pending timeouts */
hasPendingTimeouts: boolean;
/** Whether there are pending intervals */
hasPendingIntervals: boolean;
}Builder for creating complex component selection criteria.
/**
* Builder for harness predicates
* @template T Harness type
*/
class HarnessPredicate<T extends ComponentHarness> {
/**
* Create a harness predicate
* @param harnessType - Harness constructor
* @param options - Initial predicate options
*/
constructor(harnessType: ComponentHarnessConstructor<T>, options: BaseHarnessFilters);
/**
* Add a custom predicate condition
* @param description - Description of the condition
* @param predicate - Predicate function
* @returns This predicate for chaining
*/
add<K extends keyof T>(
description: string,
predicate: AsyncPredicate<T>
): HarnessPredicate<T>;
/**
* Add an option-based predicate condition
* @param name - Option name
* @param option - Option value or query
* @param predicate - Predicate function for the option
* @returns This predicate for chaining
*/
addOption<K extends keyof T>(
name: string,
option: HarnessQuery<T[K]> | null,
predicate: AsyncOptionPredicate<T, T[K]>
): HarnessPredicate<T>;
/**
* Get description of this predicate
* @returns Predicate description
*/
getDescription(): string;
/**
* Evaluate this predicate against a harness
* @param harness - Harness to evaluate
* @returns Promise resolving to true if predicate matches
*/
evaluate(harness: T): Promise<boolean>;
}
/**
* Base filters for harness predicates
*/
interface BaseHarnessFilters {
/** CSS selector that must match */
selector?: string;
/** Ancestor selector that must exist */
ancestor?: string;
}
/**
* Async predicate function type
*/
interface AsyncPredicate<T> {
(harness: T): Promise<boolean>;
}
/**
* Async option predicate function type
*/
interface AsyncOptionPredicate<T, O> {
(harness: T, option: O): Promise<boolean>;
}
/**
* Harness query type
*/
type HarnessQuery<T> = string | RegExp | ((value: T) => boolean);Specific implementations for different test environments.
/**
* Harness environment for Angular TestBed
*/
class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
/**
* Create a harness loader for a component fixture
* @param fixture - Component fixture
* @returns Harness loader for the fixture
*/
static loader(fixture: ComponentFixture<unknown>): HarnessLoader;
/**
* Get a harness for the fixture's component
* @param fixture - Component fixture
* @param harnessType - Harness constructor
* @returns Promise of component harness
*/
static harnessForFixture<T extends ComponentHarness>(
fixture: ComponentFixture<unknown>,
harnessType: ComponentHarnessConstructor<T>
): Promise<T>;
protected createTestElement(element: Element): TestElement;
protected createEnvironment(element: Element): HarnessEnvironment<Element>;
protected getAllRawElements(selector: string): Promise<Element[]>;
protected requestTaskState(): Promise<TaskState>;
protected waitForTasksOutsideAngular(): Promise<void>;
}
/**
* Harness loader interface
*/
interface HarnessLoader {
/**
* Get a harness for a component
* @param harnessType - Harness constructor or predicate
* @returns Promise of component harness
*/
getHarness<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>
): Promise<T>;
/**
* Get a harness for a component or null if not found
* @param harnessType - Harness constructor or predicate
* @returns Promise of component harness or null
*/
getHarnessOrNull<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>
): Promise<T | null>;
/**
* Get all harnesses for components
* @param harnessType - Harness constructor or predicate
* @returns Promise of component harness array
*/
getAllHarnesses<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>
): Promise<T[]>;
/**
* Get child loader for a selector
* @param selector - CSS selector
* @returns Promise of child harness loader
*/
getChildLoader(selector: string): Promise<HarnessLoader>;
/**
* Get all child loaders for a selector
* @param selector - CSS selector
* @returns Promise of child harness loader array
*/
getAllChildLoaders(selector: string): Promise<HarnessLoader[]>;
}Utility functions for testing scenarios.
/**
* Execute multiple async functions in parallel
* @param values - Array of functions that return promises
* @returns Promise that resolves to array of results
*/
function parallel<T>(values: (() => T | Promise<T>)[]): Promise<T[]>;
/**
* Disable automatic change detection for manual control
* @returns Function to re-enable automatic change detection
*/
function manualChangeDetection(): () => void;
/**
* Special test keys for keyboard interactions
*/
enum TestKey {
ALT = 'alt',
BACKSPACE = 'backspace',
CONTROL = 'control',
DELETE = 'delete',
DOWN_ARROW = 'downarrow',
END = 'end',
ENTER = 'enter',
ESCAPE = 'escape',
F1 = 'f1',
F2 = 'f2',
F3 = 'f3',
F4 = 'f4',
F5 = 'f5',
F6 = 'f6',
F7 = 'f7',
F8 = 'f8',
F9 = 'f9',
F10 = 'f10',
F11 = 'f11',
F12 = 'f12',
HOME = 'home',
INSERT = 'insert',
LEFT_ARROW = 'leftarrow',
META = 'meta',
PAGE_DOWN = 'pagedown',
PAGE_UP = 'pageup',
RIGHT_ARROW = 'rightarrow',
SHIFT = 'shift',
SPACE = 'space',
TAB = 'tab',
UP_ARROW = 'uparrow'
}/**
* Angular module for testing utilities
*/
@NgModule({})
class TestingModule {}import { ComponentHarness } from '@angular/cdk/testing';
import { TestKey } from '@angular/cdk/testing';
/**
* Harness for interacting with a button component
*/
export class ButtonHarness extends ComponentHarness {
static hostSelector = 'app-button';
/**
* Get the button text
* @returns Promise resolving to button text
*/
async getText(): Promise<string> {
const host = await this.host();
return host.text();
}
/**
* Click the button
* @returns Promise that resolves when click is complete
*/
async click(): Promise<void> {
const host = await this.host();
return host.click();
}
/**
* Check if button is disabled
* @returns Promise resolving to true if disabled
*/
async isDisabled(): Promise<boolean> {
const host = await this.host();
return host.hasClass('disabled');
}
/**
* Get button type
* @returns Promise resolving to button type
*/
async getType(): Promise<string | null> {
const host = await this.host();
return host.getAttribute('type');
}
/**
* Focus the button and press enter
* @returns Promise that resolves when action is complete
*/
async focusAndPressEnter(): Promise<void> {
const host = await this.host();
await host.focus();
await host.sendKeys(TestKey.ENTER);
}
}import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ButtonHarness } from './button.harness';
import { ButtonComponent } from './button.component';
describe('ButtonComponent', () => {
let component: ButtonComponent;
let fixture: ComponentFixture<ButtonComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ButtonComponent]
}).compileComponents();
fixture = TestBed.createComponent(ButtonComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should display button text', async () => {
component.text = 'Click me';
fixture.detectChanges();
const button = await loader.getHarness(ButtonHarness);
const text = await button.getText();
expect(text).toBe('Click me');
});
it('should handle click events', async () => {
spyOn(component, 'onClick');
const button = await loader.getHarness(ButtonHarness);
await button.click();
expect(component.onClick).toHaveBeenCalled();
});
it('should be disabled when disabled property is true', async () => {
component.disabled = true;
fixture.detectChanges();
const button = await loader.getHarness(ButtonHarness);
const isDisabled = await button.isDisabled();
expect(isDisabled).toBe(true);
});
});export interface FormHarnessFilters extends BaseHarnessFilters {
title?: string;
}
/**
* Harness for a form component
*/
export class FormHarness extends ComponentHarness {
static hostSelector = 'app-form';
private _title = this.locatorFor('h2');
private _submitButton = this.locatorFor(ButtonHarness.with({ text: 'Submit' }));
private _inputs = this.locatorForAll('input');
/**
* Predicate for filtering form harnesses
*/
static with(options: FormHarnessFilters): HarnessPredicate<FormHarness> {
return new HarnessPredicate(FormHarness, options)
.addOption('title', options.title, async (harness, title) => {
const titleElement = await harness._title();
const titleText = await titleElement.text();
return titleText === title;
});
}
/**
* Get form title
*/
async getTitle(): Promise<string> {
const title = await this._title();
return title.text();
}
/**
* Submit the form
*/
async submit(): Promise<void> {
const button = await this._submitButton();
return button.click();
}
/**
* Fill form field
*/
async fillField(name: string, value: string): Promise<void> {
const inputs = await this._inputs();
for (const input of inputs) {
const nameAttr = await input.getAttribute('name');
if (nameAttr === name) {
await input.setInputValue(value);
return;
}
}
throw new Error(`Field '${name}' not found`);
}
/**
* Get all form data
*/
async getFormData(): Promise<Record<string, string>> {
const inputs = await this._inputs();
const data: Record<string, string> = {};
for (const input of inputs) {
const name = await input.getAttribute('name');
const value = await input.getProperty('value');
if (name) {
data[name] = value;
}
}
return data;
}
}import { parallel } from '@angular/cdk/testing';
it('should handle multiple interactions simultaneously', async () => {
const button1 = await loader.getHarness(ButtonHarness.with({ text: 'Button 1' }));
const button2 = await loader.getHarness(ButtonHarness.with({ text: 'Button 2' }));
const button3 = await loader.getHarness(ButtonHarness.with({ text: 'Button 3' }));
// Click all buttons in parallel
await parallel(() => [
button1.click(),
button2.click(),
button3.click()
]);
// Verify all buttons were clicked
// ... assertions
});