GuideUpdated in v15.3

Testing

Drive ngwr components from your specs with Angular CDK test harnesses.

First test

Import a harness from ngwr/<name>/testing and load it from the fixture.

import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { TestBed } from '@angular/core/testing';
import { WrButtonHarness } from 'ngwr/button/testing';

it('disables Save until the form is filled', async () => {
  const fixture = TestBed.createComponent(CheckoutPage);
  fixture.detectChanges();

  const loader = TestbedHarnessEnvironment.loader(fixture);
  const save = await loader.getHarness(WrButtonHarness.with({ text: 'Save' }));

  expect(await save.isDisabled()).toBe(true);
});

Every method is async. Await each call; you need no detectChanges() between them.

Finding a harness

with() narrows the query: a string matches exactly, a RegExp is tested, and options combine.

await loader.getHarness(WrButtonHarness.with({ text: 'Save' }));
await loader.getAllHarnesses(WrButtonHarness.with({ text: /^S/ }));
await loader.getAllHarnesses(WrButtonHarness.with({ disabled: true }));

await loader.getHarness(WrCheckboxHarness.with({ label: 'I agree' }));
await loader.getHarness(WrInputHarness.with({ placeholder: 'Email' }));
await loader.getHarness(WrSwitchHarness.with({ label: 'Dark mode', on: false }));
MethodWhen nothing matches
getHarness()Throws
getHarnessOrNull()Returns null
getAllHarnesses()Returns []

A form, end to end

it('enables Save once the form is valid', async () => {
  const loader = TestbedHarnessEnvironment.loader(fixture);

  const email = await loader.getHarness(WrInputHarness.with({ placeholder: 'Email' }));
  const terms = await loader.getHarness(WrCheckboxHarness.with({ label: 'I agree' }));
  const save = await loader.getHarness(WrButtonHarness.with({ text: 'Save' }));

  await email.setValue('[email protected]');
  await terms.check();

  expect(await save.isDisabled()).toBe(false);
});

Overlays

Pick the loader by whether the component is an element in your template.

const loader = TestbedHarnessEnvironment.loader(fixture);
const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);

it('confirms before deleting', async () => {
  await (await loader.getHarness(WrButtonHarness.with({ text: 'Delete' }))).click();

  const dialog = await rootLoader.getHarness(WrDialogHarness);
  expect(await dialog.getTitleText()).toBe('Delete item');

  // Searches inside this dialog only.
  await (await dialog.getHarness(WrButtonHarness.with({ text: 'Confirm' }))).click();

  const toast = await rootLoader.getHarness(WrToastHarness.with({ type: 'success' }));
  expect(await toast.getMessage()).toBe('Item deleted');
});
LoaderUse for
loader(fixture) Anything in your template. Overlays you open from it (select, dropdown, popover, …) find their own panel.
documentRootLoader(fixture)Dialog, toast, drawer, window and tour.

A harness looked up in the wrong loader fails with “failed to find element”.

Select

ngwr/select/testing

it('picks a size', async () => {
  const select = await loader.getHarness(WrSelectHarness);

  await select.open();
  expect(await select.getOptionLabels()).toEqual(['Small', 'Medium', 'Large']);

  await select.selectOption({ text: 'Medium' });
  expect(await select.getValueText()).toBe('Medium');
});

it('filters as you type', async () => {
  const select = await loader.getHarness(WrSelectHarness);

  await select.open();
  await select.typeSearch('la');
  expect(await select.getOptionLabels()).toEqual(['Large']);
});

it('builds up a multi selection', async () => {
  const select = await loader.getHarness(WrSelectHarness);

  await select.selectOption({ text: 'Small' });
  await select.selectOption({ text: 'Large' });
  expect(await select.getChipLabels()).toEqual(['Small', 'Large']);

  await select.removeChip('Small');
  expect(await select.getChipLabels()).toEqual(['Large']);
});
NameDescriptionTypeDefault
open() / close()open() throws when no panel appears, as in tag mode or before minChars.Promise<void>—
isOpen()Whether the panel shows.Promise<boolean>—
getValueText()The selection as shown: chips joined by ', ', the label, or the search text.Promise<string>—
getPlaceholder()The placeholder, or null when a selection hides it.Promise<string | null>—
getOptions(filters?)Reachable options; throws while closed. Filters: text, selected, disabled.Promise<WrOptionHarness[]>—
getOptionLabels()Those options as strings.Promise<string[]>—
selectOption(filters)Opens if needed, then clicks the first match.Promise<void>—
typeSearch(query)Replaces the search or tag query.Promise<void>—
getChipLabels() / removeChip(label)Chips in multi and tag modes, without the +N more chip.Promise<string[]> | Promise<void>—
clear()Clicks the clear control.Promise<void>—
getNoResultsText() / isLoading()The empty and loading rows.Promise<string | null> | Promise<boolean>—
getLoadErrorText() / retryLoad()The row a failed [loader] shows, and its retry under retryable, which throws when none is on offer.Promise<string | null> | Promise<void>—
isMultiple() / isDisabled() / focus()Mode and state.Promise<boolean> | Promise<void>—
Option: getText() / hasLeading()WrOptionHarness: the drawn text, and whether a wrOptionLeading visual shows.Promise<string> | Promise<boolean>—
Option: isSelected() / isDisabled() / isActive() / isHidden()isActive is the keyboard cursor; isHidden means filtered out.Promise<boolean>—
Option: click()Clicks the option.Promise<void>—

Dialog

ngwr/dialog/testing

NameDescriptionTypeDefault
getTitleText() / getContentText()The title and content text, or null.Promise<string | null>—
getHarness(…)Finds a harness inside this dialog only.Promise<T>—
getRole() / isModal()Read from the overlay element.Promise<string | null> | Promise<boolean>—
isClosable() / getCloseLabel() / close()close() throws when opened with closable: false.Promise<boolean> | Promise<string | null> | Promise<void>—
sendEscape()Presses Escape; ignored with closeOnEscape: false.Promise<void>—
isFocusTrapped()Whether focus is inside the dialog.Promise<boolean>—

Toast

ngwr/toast/testing

NameDescriptionTypeDefault
getMessage() / getTitle()The text; getTitle() is null without a title, getMessage() is '' without a message.Promise<string> | Promise<string | null>—
getType()The intent.Promise<WrToastType | null>—
getRole() / getLiveLevel()Danger: alert / assertive. Warning: status / assertive. Others: status / polite.Promise<string | null>—
isDismissible() / dismiss()dismiss() throws when shown with dismissible: false.Promise<boolean> | Promise<void>—
hasCopyAction() / copy()The copy button, with showCopy: true.Promise<boolean> | Promise<void>—
hasProgressBar()Needs showProgress and a non-zero duration.Promise<boolean>—
hover() / mouseAway()Hover pauses auto-dismiss.Promise<void>—

Button

ngwr/button/testing

NameDescriptionTypeDefault
getText()The visible label.Promise<string>—
isDisabled()true when disabled, aria-disabled or loading.Promise<boolean>—
isLoading()Whether the spinner shows.Promise<boolean>—
getColor()The intent, or null.Promise<WrColor | null>—
click()Clicks the button.Promise<void>—
focus() / isFocused()Focus.Promise<void> | Promise<boolean>—

Input

ngwr/input/testing

NameDescriptionTypeDefault
getValue()The current value.Promise<string>—
setValue(value)Types a value: fires input, then change.Promise<void>—
clear()Empties the field.Promise<void>—
getPlaceholder()The placeholder.Promise<string>—
isDisabled() / isReadonly()Field state.Promise<boolean>—
isInvalid()Whether aria-invalid is set.Promise<boolean>—
getTagName()'input' or 'textarea'.Promise<string>—
focus() / blur() / isFocused()Focus.Promise<void> | Promise<boolean>—

Checkbox

ngwr/checkbox/testing

NameDescriptionTypeDefault
getLabel()The label.Promise<string>—
isChecked() / isIndeterminate() / isDisabled()Box state.Promise<boolean>—
getCheckboxValue()The checkboxValue group identity, or null when it is not a primitive.Promise<string | null>—
toggle() / check() / uncheck()check and uncheck do nothing when already there.Promise<void>—
focus() / isFocused()Focus.Promise<void> | Promise<boolean>—

Switch

ngwr/switch/testing

NameDescriptionTypeDefault
getLabel()The label.Promise<string>—
isOn() / isDisabled()Switch state.Promise<boolean>—
getRole()'switch'.Promise<string | null>—
toggle() / turnOn() / turnOff()turnOn and turnOff do nothing when already there.Promise<void>—
focus() / isFocused()Focus.Promise<void> | Promise<boolean>—

Dropdown

ngwr/dropdown/testing

it('renames from the menu', async () => {
  const menu = await loader.getHarness(WrDropdownHarness.with({ text: 'Actions' }));

  await menu.open();
  expect(await menu.getItemTexts()).toEqual(['Rename', 'Duplicate', 'Delete']);
  expect(await menu.getFocusedItemText()).toBe('Rename');

  await menu.clickItem({ text: 'Rename' });
  expect(await menu.isOpen()).toBe(true);   // picking does not close the menu
});
NameDescriptionTypeDefault
open() / openByKeyboard() / close()open() works in any trigger mode; openByKeyboard() presses ArrowDown.Promise<void>—
isOpen() / getTriggerText()Menu state and trigger label.Promise<boolean> | Promise<string>—
getItems(filters?) / getItemTexts()The menu items. Filters: text, disabled.Promise<WrDropdownItemHarness[]> | Promise<string[]>—
clickItem(filters)Opens if needed, then clicks the first match. The menu stays open.Promise<void>—
getFocusedItemText()Where the roving focus is.Promise<string | null>—
getMenuRole() / isMenuLabelledByTrigger()The menu role and name.Promise<string | null> | Promise<boolean>—
clickTrigger() / hoverTrigger() / mouseAwayFromTrigger()Single gestures, to test one trigger mode.Promise<void>—
Item: getText() / hasIcon() / getIconName()WrDropdownItemHarness: the label and its leading icon.Promise<…>—
Item: getRole() / isDisabled() / isFocused() / click()Role, state and click.Promise<…>—

Date picker

ngwr/date-picker/testing

// Pin the locale your assertions are written in.
TestBed.configureTestingModule({
  providers: [provideWr(withDates({ locale: 'en-US' }))],
});

it('picks a departure date', async () => {
  const picker = await loader.getHarness(WrDatePickerHarness);

  await picker.open();
  expect(await picker.getPanelHeader()).toBe('January 2025');
  expect(await picker.getWeekdayLabels()).toEqual(['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']);

  await picker.selectDay(20);
  expect(await picker.getValueText()).toBe('20.01.2025');
});

it('reads the state of a day cell', async () => {
  const picker = await loader.getHarness(WrDatePickerHarness);
  await picker.open();

  const day = await picker.getDay(15);
  expect(await day.isSelected()).toBe(true);
  expect(await day.isToday()).toBe(false);

  await expect(picker.selectDay(5)).rejects.toThrow(/disabled/);
});

it('sets a time and steps it', async () => {
  const picker = await loader.getHarness(WrDatePickerHarness);   // mode="datetime"
  await picker.open();

  await picker.setTime({ hours: 9, minutes: 30 });
  await picker.stepTime('minutes', 1);
  expect(await picker.getTime()).toBe('09:31');
});
NameDescriptionTypeDefault
open() / close() / isOpen()close() toggles this picker's own trigger.Promise<void> | Promise<boolean>—
getValueText() / setValueText(text) / clear()The field text; setValueText types it.Promise<string> | Promise<void>—
getPanelHeader() / getView() / getWeekdayLabels()getView() is day, month or year.Promise<string> | Promise<string[]>—
next() / previous() / zoomOut()One step of the current view.Promise<void>—
selectMonth(month) / selectYear(year)A month takes a 0-based index or its label.Promise<void>—
getDays(filters?) / getDay(n) / selectDay(n)selectDay throws on a disabled day. Filters: text, selected, disabled, inRange.Promise<WrDatePickerDayHarness[]> | Promise<void>—
getTime() / setTime(fields) / stepTime(unit, ±1) / toggleMeridiem()time and datetime modes only.Promise<string> | Promise<void>—
getMode() / isDisabled() / isReadonly() / focus() / blur() / isFocused()Mode and field state.Promise<…>—
Day: getDayOfMonth() / getText()WrDatePickerDayHarness: the day number.Promise<number> | Promise<string>—
Day: isSelected() / isDisabled() / isToday() / isInRange() / isOutOfMonth() / isActive()Cell state. isActive is the tab stop, not the selection.Promise<boolean>—
Range: getStartText() / getEndText() / getSeparator()WrDateRangePickerHarness: the two fields and the separator.Promise<string>—
Range: setStartText(text) / setEndText(text) / clear()Types into one end.Promise<void>—
Range: focus(end) / blur(end) / isFocused(end) / getPlaceholder(end)end is 'start' or 'end'.Promise<…>—
Range: getTime(end) / setTime(end, fields) / stepTime(end, unit, ±1) / toggleMeridiem(end)Time per end. The panel methods above work on both pickers.Promise<…>—

Popover and tooltip

ngwr/popover/testing

it('explains itself on hover', async () => {
  const tip = await loader.getHarness(WrPopoverHarness.with({ mode: 'tooltip' }));

  await tip.open();                       // hover for a tooltip, click for a popover
  expect(await tip.getContentText()).toBe('Save changes');
  expect(await tip.getRole()).toBe('tooltip');

  await tip.close();
});
NameDescriptionTypeDefault
getMode()popover or tooltip.Promise<'popover' | 'tooltip'>—
open(timeout?) / close(timeout?)The right gesture for the mode, then waits for the panel.Promise<void>—
waitUntilOpen(timeout?) / waitUntilClosed(timeout?)Only the wait.Promise<void>—
click() / hover() / mouseAway() / focus() / blur() / sendEscape()One gesture, no wait.Promise<void>—
getContentText() / getDescriptionText()The panel text, and a tooltip's aria-describedby text.Promise<string> | Promise<string | null>—
getRole() / getLabel() / isModal()tooltip or dialog.Promise<…>—
getPosition()The placement.Promise<WrPopoverPosition | null>—

Drawer

ngwr/drawer/testing

it('closes the drawer', async () => {
  const drawer = await rootLoader.getHarness(WrDrawerHarness);

  expect(await drawer.getTitleText()).toBe('Filters');
  expect(await drawer.getPosition()).toBe('end');
  expect(await drawer.isLabelledByTitle()).toBe(true);

  await drawer.close();
});
NameDescriptionTypeDefault
isOpen()Whether the pane is attached.Promise<boolean>—
getTitleText() / getContentText()Title and content text.Promise<string | null>—
getPosition() / isSheet() / isRounded() / hasSafeArea()Edge and presentation.Promise<…>—
getRole() / isModal() / isLabelledByTitle()Read from the overlay element.Promise<…>—
isClosable() / getCloseLabel() / close() / sendEscape()close() throws when opened with closable: false.Promise<…>—
hasBackdrop() / clickBackdrop()This drawer's backdrop.Promise<boolean> | Promise<void>—
isFocusTrapped()Whether focus is inside the drawer.Promise<boolean>—

Colour picker

ngwr/color-picker/testing

it('picks a colour through the fields', async () => {
  const trigger = await loader.getHarness(WrColorPickerTriggerHarness);
  const picker = await trigger.open();

  await picker.setHex('#3969e2');
  expect(await picker.getHex()).toBe('#3969e2ff');

  await picker.setTab('rgb');
  expect(await picker.getRgb()).toEqual({ r: 57, g: 105, b: 226 });

  await picker.setAlphaPercent(50);
  expect((await picker.getThumbs()).alpha).toBe(50);
});
NameDescriptionTypeDefault
getHex() / setHex(text) / blurHex() / isHexFocused()The colour as hex, 8 digits with alpha. Not the same as value.Promise<…>—
getTab() / setTab(tab) / getTabs()The field tab; getTabs() returns a WrSegmentedHarness.Promise<…>—
getRgb() / setRgbChannel(ch, n) / getHsl() / setHslChannel(ch, n)Channels of the active tab; the other tab throws.Promise<…>—
getAlphaPercent() / setAlphaPercent(n) / hasAlpha()Alpha in percent; null without alpha. Throws on the HEX tab.Promise<…>—
getSwatches() / pickSwatch(color) / getThumbs() / isDisabled()Presets by the string you passed, and the thumb positions.Promise<…>—
Trigger: isOpen() / open() / close() / toggle() / sendEscape() / getPicker() / isDisabled()WrColorPickerTriggerHarness. open() returns the picker.Promise<…>—

Splitter

ngwr/splitter/testing

it('resizes from the keyboard', async () => {
  const splitter = await loader.getHarness(WrSplitterHarness);

  // Side-by-side panes have a vertical divider.
  expect(await splitter.getOrientation()).toBe('horizontal');
  expect(await splitter.getDividerOrientation()).toBe('vertical');

  await splitter.setPosition(70);
  expect(await splitter.getPaneSizes()).toEqual({ start: 70, end: 30 });

  await splitter.pressHome();
  expect(await splitter.getPosition()).toBe(await splitter.getMinPosition());
});
NameDescriptionTypeDefault
getPosition() / getMinPosition() / getMaxPosition()The divider's aria-value* numbers.Promise<number>—
getOrientation() / getDividerOrientation()Opposites: side-by-side panes have a vertical divider.Promise<…>—
pressArrow(arrow, { shift }) / pressHome() / pressEnd() / setPosition(n)The key pressed, not a direction: in RTL, ArrowRight shrinks. setPosition throws if it cannot land.Promise<void>—
getPaneSizes() / getStartText() / getEndText()Pane sizes, from the inline flex-basis.Promise<…>—
isDisabled() / isDividerFocusable() / focusDivider() / isDividerFocused() / getDividerLabel()Divider state and focus.Promise<…>—

Charts

ngwr/charts/<name>/testing

it('reads the data, not the drawing', async () => {
  const bars = await loader.getHarness(WrBarChartHarness);

  expect(await bars.getLabels()).toEqual(['Mon', 'Tue', 'Wed']);
  expect(await bars.getAccessibleNames()).toEqual(['Mon: 12', 'Tue: 24', 'Wed: 6']);
  expect((await bars.getBars()).map(b => b.heightPercent)).toEqual([50, 100, 25]);

  const donut = await loader.getHarness(WrDonutChartHarness);
  expect(await donut.getLegend()).toEqual([{ label: 'Direct', value: '30' }]);
});
NameDescriptionTypeDefault
Every chart: getAccessibleName()The name the chart announces. On all sixteen chart harnesses and the legend harness.Promise<string | null>—
Data table: hasDataTable() / getTableColumns() / getTableRows()The hidden dataTable on the bar, line, scatter, radar, heatmap chart, calendar heatmap, funnel, waterfall, treemap, box plot and candlestick harnesses. The table reads throw while dataTable is off.Promise<…>—
Bar: getBars() / getLabels() / getAccessibleNames() / getBarCount() / hasValues() / getPlotHeight() / hasLegend()WrBarChartHarness. Heights are a percent of the chart's maximum. Filters: ariaLabel, barLabel.Promise<…>—
Bar: getOrientation() / isStacked() / getSeriesBars() / getValues() / getZeroPercent() / getLines() / getLineCount()Series, stacked, horizontal and combo modes.Promise<…>—
Line: getSeriesLabels() / getLineCount() / getDotCount() / getYTicks() / getXLabels() / getPlotHeight() / hasGrid()WrLineChartHarness. Filters: ariaLabel, seriesLabel.Promise<…>—
Line: hasLegend() / hasArea() / getAreaCount() / isStacked()The legend, and the area and stacked modes.Promise<…>—
Line: hasTooltip() / getTooltipLabel() / getTooltipRows()Reads the tooltip. There is no hoverAt().Promise<…>—
Donut: getSliceCount() / getLegend() / getCenterValue() / getCenterLabel() / getSize() / isPie() / hasLegend()WrDonutChartHarness. isPie() reads the pie mode. Filters: ariaLabel, sliceLabel.Promise<…>—
Sparkline: isDecorative() / getRole() / hasLine() / hasArea() / hasTip() / getColor()WrSparklineHarness. Named is role="img", unnamed is aria-hidden. Filters: ariaLabel.Promise<…>—
Gauge: getRole() / getValue() / getMin() / getMax() / getValueText() / getDisplayValue() / getSuffix() / getSize()WrGaugeHarness. Filters: ariaLabel, value.Promise<…>—
Meter group: getRole() / getValue() / getMax() / getSlices() / getLegendLabels() / getLegendValues() / hasLegend()WrMeterGroupHarness. Filters: ariaLabel, sliceLabel.Promise<…>—
Calendar heatmap: getRole() / getCellCount() / getCells() / getValueFor(iso) / getWeekdayLabels() / getMonthLabels() / hasLabels()WrCalendarHeatmapHarness. Filters: ariaLabel.Promise<…>—
Scatter: getPoints() / getPointCount() / getSeriesLabels() / hasLegend()WrScatterChartHarness. Points are read from the markers; there is no hover method. Filters: ariaLabel, seriesLabel.Promise<…>—
Scatter: getXTicks() / getYTicks() / getAxisTitles() / getPlotHeight() / hasGrid()The axes.Promise<…>—
Radar: getAxisLabels() / getRingLabels() / getPolygons() / getPolygonCount() / getAreaCount() / getDotCount() / getSize()WrRadarChartHarness. getPolygons() gives each outline's colour, dash and whether it closes. Filters: ariaLabel, axisLabel.Promise<…>—
Radar: hasGrid() / hasLegend()The rings and spokes, and the legend.Promise<boolean>—
Radar: getVariant()'polygon' or 'polar-area', read off the host's modifier class.Promise<WrRadarVariant>—
Heatmap chart: getRowLabels() / getColumnLabels() / getCells() / getCell(row, column) / getCellCount()WrHeatmapChartHarness. Cells are read from what the component wrote: value, level, empty. No hover method. Filters: ariaLabel, rowLabel, columnLabel.Promise<…>—
Heatmap chart: hasLegend() / getScaleRange() / getLevelCount() / getNoDataLabel()The colour scale. Its reads throw when the chart draws none.Promise<…>—
Funnel: getStages() / getStageCount() / getLabels() / getGapLabels() / getBandCount() / hasValues()WrFunnelChartHarness. Each stage carries its value and both conversions; there is no hover method. Filters: ariaLabel, stageLabel.Promise<…>—
Waterfall: getSteps() / getStepCount() / getLabels() / getConnectorCount() / hasValues()WrWaterfallChartHarness. Each step's kind, running totals and bar ends in percent; there is no hover method. Filters: ariaLabel, stepLabel.Promise<…>—
Waterfall: getYTicks() / getZeroPercent() / getPlotHeight() / hasGrid()The value axis.Promise<…>—
Treemap: getTiles() / getTileCount() / getGroupCount() / getLeafCount() / getLabels() / getPlotHeight()WrTreemapChartHarness. getTiles() gives each tile's name, value, kind, colour and box; no hover method. Filters: ariaLabel, tileLabel.Promise<…>—
Bullet: getLabel() / getSubLabel() / getValueExtent() / getTargetPosition() / getRanges() / getScaleLabels() / getDescription() / hasScale()WrBulletChartHarness. Extents and the target are shares of the scale, from 0 to 1. Filters: ariaLabel, label.Promise<…>—
Box plot: getGroups() / getGroupLabels() / getGroupCount() / getYTicks() / getAxisTitles() / getPlotHeight() / hasGrid()WrBoxplotChartHarness. Each group's five numbers, outliers and positions; there is no hover method. Filters: ariaLabel, groupLabel.Promise<…>—
Candlestick: getCandles() / getCandleCount() / isDense() / getYTicks() / getXLabels() / getPlotHeight() / hasGrid()WrCandlestickChartHarness. Each candle's prices and direction; there is no hover method. Filters: ariaLabel.Promise<…>—
Legend: getItems() / getLabels() / getItemCount() / getHidden() / isToggleable() / toggle(index)WrChartLegendHarness, standalone or from getLegendHarness() on the bar, line, donut, meter group, scatter and radar harnesses. Filters: ariaLabel, itemLabel.Promise<…>—

Event calendar

ngwr/event-calendar/testing

it('moves an event with the keyboard', async () => {
  const calendar = await loader.getHarness(WrEventCalendarHarness);

  expect(await calendar.getView()).toBe('month');
  expect(await calendar.getChipLabels()).toHaveLength(3);

  const [standup] = await calendar.getChips({ title: 'Standup' });
  await standup.move('right');

  // The calendar only emits eventChange; the host applies it.
  expect(changed()?.kind).toBe('move');
  expect(changed()?.start.getDate()).toBe(15);
});
NameDescriptionTypeDefault
getView() / getTitle() / previous() / next() / goToday() / setView(label) / getViewLabels() / getActiveViewLabel()Navigation. setView takes the label the switcher shows.Promise<…>—
getChips(filters?) / getChipLabels() / getCellChips(day, minutes?)One chip per event, in its start cell. Filters: label, title, band.Promise<…>—
clickCell(day, minutes?) / getCellLabel(day, minutes?) / focusCell(day, minutes?) / getCursor() / pressArrow(arrow) / pressHome() / pressEnd()minutes from midnight, or -1 for a month cell or the all-day row.Promise<…>—
getWeekdayNames() / getDayNumbers() / getSlotLabels() / hasAllDayRow() / getOverflowLabels() / openOverflow()Grid labels and the “+N more” button.Promise<…>—
Chip: getLabel() / getTitle() / getTime() / isBand() / continuesBefore() / continuesAfter() / click() / move(arrow) / resize(arrow)WrEventCalendarChipHarness. move() is Alt + arrow; resize() adds Shift.Promise<…>—

Stub document.elementFromPoint before you click an editable chip.

Image cropper

ngwr/image-cropper/testing

it('opens a crop window once the image is measured', async () => {
  const cropper = await loader.getHarness(WrImageCropperHarness);

  // jsdom has no layout: give the image a size first.
  const img = fixture.nativeElement.querySelector('.wr-image-cropper__image');
  img.getBoundingClientRect = () => ({ width: 400, height: 400, x: 0, y: 0 });
  Object.defineProperty(img, 'naturalWidth', { value: 800 });
  Object.defineProperty(img, 'naturalHeight', { value: 800 });

  await cropper.dispatchImageLoad();

  expect(await cropper.getCropBox()).toEqual({ x: 80, y: 80, width: 240, height: 240 });
});
NameDescriptionTypeDefault
isEmpty() / getEmptyText() / getImageSrc() / isReady()A cropper with a src is not ready until its image is measured.Promise<…>—
dispatchImageLoad()Fires load. Stub the image's getBoundingClientRect() and naturalWidth first.Promise<void>—
getCropBox() / getHandles() / hasBackdrop()The crop window in display pixels.Promise<…>—

Window

ngwr/window/testing

it('minimizes to the taskbar and back', async () => {
  windows.open(EditorComponent, { title: 'Untitled.md' });
  await fixture.whenStable();

  const win = await rootLoader.getHarness(WrWindowHarness.with({ title: 'Untitled.md' }));
  await win.minimize();

  expect(await win.isOpen()).toBe(true);   // minimized is still open
  expect(await win.getState()).toBe('minimized');

  const taskbar = await loader.getHarness(WrWindowTaskbarHarness);
  expect(await taskbar.getTabTitles()).toEqual(['Untitled.md']);

  await taskbar.restore('Untitled.md');
  expect(await win.getState()).toBe('normal');
});
NameDescriptionTypeDefault
getTitle() / getState() / isOpen() / getBodyText() / getStatusBarText() / getTitleExtraText()isOpen() stays true while minimized.Promise<…>—
close() / minimize() / maximize() / doubleClickChrome() / sendEscape() / focusWindow()A button the chrome does not render throws.Promise<void>—
getBox() / getZIndex() / isResizable() / getResizeHandles()Position, size and stack order.Promise<…>—
getCloseLabel() / getMinimizeLabel() / getMaximizeLabel() / hasCloseButton() / hasMinimizeButton() / hasMaximizeButton() / getOs() / getChromeSize()The chrome buttons; labels change with state and OS.Promise<…>—
getRole() / isLabelledByTitle() / isHiddenFromAssistiveTech() / isActive()A non-modal dialog named by its title. isActive() is the window the keyboard is in.Promise<…>—
startKeyboardMove() / startKeyboardResize() / getKeyboardMode() / getAnnouncement()Alt+F7 / Alt+F8. Then sendKeys arrows, Enter or Escape to the window.Promise<…>—
Taskbar: isEmpty() / getTabTitles() / restore(title) / closeTab(title) / getRestoreLabel(title) / getRailLabel() / getPosition()WrWindowTaskbarHarness. closeTab() closes without restoring.Promise<…>—
Taskbar: getActiveTabTitle() / isTabPressed(title)With show="all": the active window's tab, the one with aria-pressed="true".Promise<string | null> | Promise<boolean>—

Calendar

ngwr/calendar/testing

it('picks a day and walks the views', async () => {
  const calendar = await loader.getHarness(WrCalendarHarness);

  expect(await calendar.getHeaderLabel()).toBe('March 2026');
  await (await calendar.getDay(20)).click();
  expect(await calendar.getSelectedDayNumbers()).toEqual([20]);

  await calendar.clickHeader();               // day -> month
  await calendar.selectChip('Jun');           // and back down
  expect(await calendar.getHeaderLabel()).toBe('June 2026');
});
NameDescriptionTypeDefault
getView() / getHeaderLabel() / clickHeader() / previous() / next()The header walks day, month, year; clickHeader() throws at the top.Promise<…>—
getDays(filters?) / getDay(n) / getSelectedDayNumbers() / getInRangeDayNumbers() / getActiveDayNumber()Days of the shown month. getActiveDayNumber() can be null after paging. Filters: text, selected, disabled, inRange.Promise<…>—
getMonths() / getYears() / selectChip(label)The month and year views; throws in the day view.Promise<…>—
pressArrow(arrow) / pressHome() / pressEnd() / pressPageUp({ shift }) / pressPageDown({ shift }) / pressEnter()Home and End go to the ends of the week; shift pages a year.Promise<void>—
getWeekdayNames() / getWeekCount() / getGridRole() / getPreviousLabel() / getNextLabel() / getMode() / isDisabled()Shape and labels.Promise<…>—
Day: getText() / getAccessibleName() / getDayOfMonth() / isSelected() / isDisabled() / isToday() / isOutOfMonth() / isInRange() / isActive() / click()WrCalendarDayHarness, also exported as WrDatePickerDayHarness.Promise<…>—

Tour

ngwr/tour/testing

it('walks the tour', async () => {
  tour.start(steps);
  await fixture.whenStable();

  const first = await rootLoader.getHarness(WrTourHarness);
  expect(await first.getTitle()).toBe('Search');
  expect(await first.hasBack()).toBe(false);

  await first.next();

  // Each step is a new card, so get a fresh harness.
  const second = await rootLoader.getHarness(WrTourHarness);
  expect(await second.getProgress()).toEqual({ current: 2, total: 2 });
  expect(await second.getPrimaryLabel()).toBe('Done');
});
NameDescriptionTypeDefault
getTitle() / getContent() / getProgressText() / getProgress()The step. getProgress() counts skipped steps too.Promise<…>—
next() / back() / skipTour() / hasBack()back() throws on the first step.Promise<…>—
getPrimaryLabel() / getBackLabel() / getSkipLabel()getPrimaryLabel() reads “Done” on the last reachable step.Promise<string>—
isShowing() / isModal() / getAccessibleName()isShowing() turns false when the step is torn down.Promise<…>—

Lightbox

ngwr/lightbox/testing

it('opens the full image', async () => {
  const lightbox = await loader.getHarness(WrLightboxHarness.with({ alt: 'Mountain' }));

  await expect(lightbox.getFullSrc()).rejects.toThrow();   // no viewer while closed

  await lightbox.open();
  expect(await lightbox.isModal()).toBe(true);
  expect(await lightbox.getFullSrc()).toContain('/photo.jpg');

  await lightbox.sendEscape();
  expect(await lightbox.isOpen()).toBe(false);
});
NameDescriptionTypeDefault
isOpen() / open() / close() / clickImage() / sendEscape()Open state, and the ways in and out.Promise<…>—
getAlt() / getThumbSrc() / getFullSrc()Alt text, thumbnail and full image.Promise<string>—
getCaption() / getViewerLabel() / getCloseLabel() / isModal() / isFocusTrapped()The viewer; throws while closed.Promise<…>—
isInteractive() / getOpenLabel() / isLoading() / isViewerBound()isInteractive() is false with disablePreview.Promise<…>—

Speed dial

ngwr/speed-dial/testing

it('fans out and picks', async () => {
  const dial = await loader.getHarness(WrSpeedDialHarness);

  await expect(dial.getActions()).rejects.toThrow();   // closed

  await dial.open();
  expect(await dial.getActionLabels()).toEqual(['Share', 'Copy link']);

  await dial.sendEscape();
  expect(await dial.isTriggerFocused()).toBe(true);
});
NameDescriptionTypeDefault
isOpen() / open() / close() / toggle() / sendEscape()Escape closes and focuses the trigger.Promise<…>—
getActions(filters?) / getActionLabels() / pick(filters) / getActionCount()getActions() and getActionLabels() throw while closed; pick() opens first. Filters: label, disabled.Promise<…>—
getTriggerLabel() / getTriggerIcon() / getDirection() / hasSafeArea() / isDisabled()The trigger and layout.Promise<…>—
getMenuRole() / isMenuBound()The trigger-to-menu link.Promise<…>—
Action: getLabel() / getInitial() / hasIcon() / getIconName() / isDisabled() / click()WrSpeedDialActionHarness. getLabel() is the name; getInitial() the drawn glyph.Promise<…>—

Knob

ngwr/knob/testing

it('turns the dial from the keyboard', async () => {
  const knob = await loader.getHarness(WrKnobHarness);

  await knob.setValue(60);
  expect(await knob.getValue()).toBe(60);
  expect(await knob.getDisplayValue()).toBe('60%');

  await knob.pressEnd();
  expect(await knob.getValue()).toBe(await knob.getMax());
});
NameDescriptionTypeDefault
getValue() / getMin() / getMax()The aria-value* numbers.Promise<number>—
getDisplayValue() / getSuffix()The printed text with suffix, or null with showValue off.Promise<string | null>—
pressArrow(arrow, { shift }) / pressHome() / pressEnd() / setValue(n)shift is ten steps. setValue throws if it cannot land exactly.Promise<void>—
getHandlePosition()The handle in viewBox units.Promise<{ x: number; y: number }>—
isDisabled() / isReadonly() / isFocusable() / focus() / blur() / getLabel()State and focus. blur() emits touch.Promise<…>—

Table

ngwr/table/testing

it('sorts, selects and expands', async () => {
  const table = await loader.getHarness(WrTableHarness);

  expect(await table.getHeaderTexts()).toEqual(['Name', 'Role']);
  expect(await table.getCellTexts()).toEqual([
    ['Ada', 'admin'],
    ['Grace', 'user'],
  ]);

  await table.sortByColumn('Name');
  expect(await table.getSortDirection('Name')).toBe('ascending');

  const [first] = await table.getRows();
  await first.select();
  expect(await first.isSelected()).toBe(true);
  expect(await table.isPartiallySelected()).toBe(true);

  await first.toggleExpand();
  expect(await table.getDetailTexts()).toEqual(['Joined 2024']);
});

it('announces a tree', async () => {
  const table = await loader.getHarness(WrTableHarness);   // childrenKey set

  expect(await table.getRole()).toBe('treegrid');

  const [root] = await table.getRows();
  expect(await root.getLevel()).toBe(1);
  expect(await root.isExpandable()).toBe(true);
});
NameDescriptionTypeDefault
getRole() / isTree()treegrid with childrenKey, else table.Promise<'treegrid' | 'table'>—
getHeaderCells(filters?) / getHeaderTexts()Columns by header title.Promise<WrTableHeaderCellHarness[]> | Promise<string[]>—
sortByColumn(title) / getSortDirection(title)One sort step. The table does not reorder items; your app does. getSortDirection() reads aria-sort: only the primary column announces a direction, and every other sortable column, a secondary key included, answers 'none'.Promise<void> | Promise<…>—
getSortIndicator(title) / getSortOrder(title)The drawn direction and the 1-based place in the sort, for every sorted column.Promise<'ascending' | 'descending' | null> | Promise<number | null>—
getRows(filters?) / getCellTexts()Rendered rows only; a virtual body renders a window.Promise<WrTableRowHarness[]> | Promise<string[][]>—
hasSelectAll() / isAllSelected() / isPartiallySelected() / toggleSelectAll()The select-all box.Promise<…>—
getGroupLabels() / isGroupCollapsed(label) / toggleGroup(label)Group bands; empty with a custom group header.Promise<…>—
getDetailTexts()Open detail rows.Promise<string[]>—
getFooterTexts() / getEmptyText() / isLoading() / isVirtual() / getAriaRowCount()getAriaRowCount() is the total row count.Promise<…>—
Row: getCells(filters?) / getCellTexts()WrTableRowHarness: the row's data cells.Promise<WrTableCellHarness[]> | Promise<string[]>—
Row: isSelectable() / isSelected() / toggleSelection() / select() / deselect()Row selection.Promise<…>—
Row: isExpandable() / isExpanded() / toggleExpand()Detail rows and tree children.Promise<…>—
Row: getLevel() / getPosInSet() / getSetSize() / getRowIndex()Tree position, per sibling group.Promise<number | null>—
Cell: getText() / getColumnTitle() / getPin()WrTableCellHarness: text, column title and pinned edge.Promise<…>—
Header cell: isSortable() / sort() / getSortDirection() / getSortIndicator() / getSortOrder() / isFilterable()WrTableHeaderCellHarness. isFilterable() needs non-empty filterItems.Promise<…>—

Radio

ngwr/radio/testing

it('answers the size question', async () => {
  const group = await loader.getHarness(WrRadioGroupHarness);

  expect(await group.getRadioLabels()).toEqual(['Small', 'Medium', 'Large']);
  expect(await group.getSelectedLabel()).toBeNull();
  expect(await group.getTabStopLabel()).toBe('Small');   // nothing picked yet

  await group.select({ label: 'Large' });
  expect(await group.getSelectedLabel()).toBe('Large');
  expect(await group.getTabStopLabel()).toBe('Large');
});
NameDescriptionTypeDefault
getRadios(filters?) / getRadioLabels()This group's radios. Filters: label, value, checked, disabled.Promise<WrRadioHarness[]> | Promise<string[]>—
getSelectedRadio() / getSelectedLabel() / select(filters)select() throws if the radio stays unchecked.Promise<…>—
getTabStopLabel() / focusTabStop() / getFocusedLabel()The tab stop: the checked radio, else the first enabled one.Promise<…>—
getName()The radios' name; throws for an empty group.Promise<string>—
getAccessibleName()From aria-labelledby, then aria-label.Promise<string | null>—
getRole() / isDisabled()radiogroup; disabled only when every radio is.Promise<…>—
Radio: getLabel() / getValue() / isChecked() / isDisabled() / getSize() / hasIcon() / isLabelBound() / check() / focus() / blur() / isFocused()WrRadioHarness. getLabel() is the text only.Promise<…>—

Textarea

ngwr/textarea/testing

For <wr-textarea>. A native <textarea wrInput> uses WrInputHarness.

NameDescriptionTypeDefault
getValue() / setValue(text) / clear()The value. Writes throw on a disabled or readonly field.Promise<string> | Promise<void>—
getLabel() / getPlaceholder()getLabel() is null with no ariaLabel or placeholder.Promise<string | null> | Promise<string>—
isDisabled() / isReadonly() / isInvalid()Read from the native element.Promise<boolean>—
isAutosizing() / hasFittedHeight() / getRows() / getSize() / getResizeDirection()Sizing. hasFittedHeight() means a height was written.Promise<…>—
focus() / blur() / isFocused()Focus.Promise<…>—

Input number

ngwr/input-number/testing

it('clamps at the maximum', async () => {
  const qty = await loader.getHarness(WrInputNumberHarness);

  await qty.setValue(3);
  expect(await qty.getValue()).toBe(3);

  await qty.increment();                    // step is 1, max is 4
  await qty.increment();
  expect(await qty.getValue()).toBe(4);
  expect(await qty.isIncrementDisabled()).toBe(true);
});

it('separates the field text from the value', async () => {
  const price = await loader.getHarness(WrInputNumberHarness);

  await price.setValueText('1 234,5');      // mid-type, not committed
  expect(await price.getValueText()).toBe('1 234,5');
  expect(await price.getValue()).toBe(1234.5);
});
NameDescriptionTypeDefault
getValue(locale?) / getValueText()The number and the shown text. getValue() throws on text the control has not accepted; pass your LOCALE_ID.Promise<number | null> | Promise<string>—
setValue(n) / setValueText(text) / clear()setValueText leaves it mid-type; setValue commits.Promise<void>—
increment() / decrement() / isIncrementDisabled() / isDecrementDisabled() / hasSteppers()The stepper buttons.Promise<…>—
stepUp() / stepDown()ArrowUp and ArrowDown.Promise<void>—
getPrefix() / getSuffix() / getPlaceholder() / getAriaLabel()Field decorations.Promise<…>—
isDisabled() / isReadonly() / focus() / blur() / isFocused()State and focus.Promise<…>—

One-time code

ngwr/input-otp/testing

it('takes a pasted code', async () => {
  const otp = await loader.getHarness(WrInputOtpHarness);

  await otp.paste('123456');
  expect(await otp.getValue()).toBe('123456');
  expect(await otp.isComplete()).toBe(true);

  await otp.backspace();
  expect(await otp.getBoxValues()).toEqual(['1', '2', '3', '4', '5', '']);
  expect(await otp.getFocusedIndex()).toBe(5);
});
NameDescriptionTypeDefault
getValue() / getBoxValues() / isComplete() / getLength()The code in the boxes. After an outside write, assert your model too.Promise<…>—
setValue(code) / type(text) / paste(code) / backspace() / clear()paste fires a real paste event.Promise<void>—
getBoxes(filters?) / getBox(i) / getFocusedIndex() / moveFocus(i)Per box. Filters: value, empty, label.Promise<…>—
isMasked() / getInputMode() / getSize() / getPlaceholder() / getLabel() / isDisabled()Presentation.Promise<…>—
focus() / blur()blur() blurs the focused box.Promise<void>—

Slider

ngwr/slider/testing

it('moves the slider', async () => {
  const slider = await loader.getHarness(WrSliderHarness);

  await slider.setValue(70);
  expect(await slider.getValue()).toBe(70);

  await slider.stepUp();
  await slider.toMax();
  expect(await slider.getValue()).toBe(await slider.getMax());
});

it('sets a range', async () => {
  const slider = await loader.getHarness(WrSliderHarness);   // range

  await slider.setRange(90, 95);
  expect(await slider.getValue()).toEqual([90, 95]);
});
NameDescriptionTypeDefault
setValue(n) / setRange(low, high)Walks with the keys; throws if the value cannot be reached.Promise<void>—
getValue()A number, or [low, high] on a range.Promise<number | readonly [number, number]>—
stepUp() / stepDown() / toMin() / toMax()Arrows, Home and End.Promise<void>—
getThumbs() / getLowThumb() / getHighThumb()A WrSliderThumbHarness per thumb.Promise<WrSliderThumbHarness[]>—
isRange() / isDisabled() / getMin() / getMax() / getLabelText() / focus() / isFocused()Shape and state.Promise<…>—

Rating

ngwr/rating/testing

it('takes a rating', async () => {
  const rating = await loader.getHarness(WrRatingHarness);

  await rating.setValue(4);
  expect(await rating.getValue()).toBe(4);

  const items = await rating.getItems();
  expect(await items[3].isFilled()).toBe(true);
  expect(await items[4].isFilled()).toBe(false);

  await rating.clear();
  expect(await rating.getValue()).toBe(0);
});
NameDescriptionTypeDefault
setValue(n) / clear() / stepUp() / stepDown()Click or keyboard.Promise<void>—
getValue() / getMax() / getCount() / getFills()getFills() is each item's fill fraction.Promise<…>—
getItems()WrRatingItemHarness: getFill(), isFilled(), isPartiallyFilled(), isInteractive(), click(), clickHalf(), hover().Promise<WrRatingItemHarness[]>—
unhover()Ends a hover preview.Promise<void>—
getRole() / getLabel() / getSize() / isReadonly() / isDisabled() / isFocusable() / focus() / blur() / isFocused()Shape, state and focus.Promise<…>—

File upload

ngwr/file-upload/testing

it('takes a dropped file and removes it', async () => {
  const upload = await loader.getHarness(WrFileUploadHarness);

  await upload.dropFiles([new File(['hello'], 'notes.txt', { type: 'text/plain' })]);
  expect(await upload.getFileNames()).toEqual(['notes.txt']);

  await upload.removeFileNamed('notes.txt');
  expect(await upload.getFileCount()).toBe(0);
});
NameDescriptionTypeDefault
selectFiles(files)Through the hidden file input.Promise<void>—
dropFiles(files) / dragOver() / dragLeave() / isDragging()A full drag: dragenter, dragover, drop.Promise<…>—
getFileNames() / getFileSizes() / getFileCount()The file list. Sizes as shown, like 4.9 KB.Promise<…>—
removeFile(i) / removeFileNamed(name)By index or exact name.Promise<void>—
getLabel() / getPickText() / getDropText() / getHelperText() / getAccept() / isMultiple() / isDisabled()Copy and settings.Promise<…>—
focus() / isFocused()Focus goes to the drop zone.Promise<…>—

Context menu

ngwr/context-menu/testing

it('copies from the context menu', async () => {
  const menu = await loader.getHarness(WrContextMenuHarness);

  await menu.open();
  expect(await menu.getItemTexts()).toEqual(['Copy', 'Cut', 'Paste']);

  await menu.clickItem({ text: 'Copy' });
  expect(await menu.isOpen()).toBe(false);
});

it('walks into a submenu', async () => {
  const menu = await loader.getHarness(WrContextMenuHarness);
  await menu.open();

  const [more] = await menu.getItems({ hasSubmenu: true });
  await more.openSubmenu();
  expect(await more.isSubmenuOpen()).toBe(true);

  await more.clickSubmenuItem({ text: 'As PNG' });
});
NameDescriptionTypeDefault
open() / rightClick() / openByLongPress() / close() / closeByOutsidePress()open() fires contextmenu. Closing waits until the pane is gone.Promise<void>—
isOpen()From the target's aria-controls.Promise<boolean>—
getItems(filters?) / getItemTexts() / clickItem(filters)Items, without dividers. Filters: text, disabled, hasSubmenu.Promise<…>—
getDividerCount()Counts role="separator".Promise<number>—
getMenuRole() / getMenuText() / getTargetText()The menu and its target.Promise<…>—
Item: hasSubmenu() / isSubmenuOpen() / openSubmenu() / openSubmenuByHover() / closeSubmenu() / getSubmenuItems() / clickSubmenuItem()WrContextMenuItemHarness. Submenus at any depth.Promise<…>—

Popconfirm

ngwr/popconfirm/testing

it('asks before deleting', async () => {
  const ask = await loader.getHarness(WrPopconfirmHarness);

  await ask.open();
  expect(await ask.getMessage()).toBe('Delete this item?');
  expect(await ask.getActionLabels()).toEqual(['Cancel', 'Delete']);

  await ask.confirm();
  expect(await ask.isOpen()).toBe(false);
  expect(host.deleted()).toBe(true);
});
NameDescriptionTypeDefault
open() / close() / confirm() / cancel() / sendEscape() / clickOutside()confirm() emits confirmed; cancel, Escape and an outside click emit cancelled.Promise<void>—
getMessage() / getConfirmText() / getCancelText() / getActionLabels()The question and button labels.Promise<…>—
getRole() / isModal() / getLabel() / getDescriptionText()A non-modal dialog; the question is its description.Promise<…>—
getConfirmColor() / getPosition() / getFocusedActionLabel() / isTriggerFocused()Intent, placement and focus.Promise<…>—

Command palette

ngwr/command-palette/testing

it('runs a command', async () => {
  const palette = await loader.getHarness(WrCommandPaletteHarness);

  await palette.open();
  await palette.setQuery('set');
  expect(await palette.getItemLabels()).toEqual(['Settings', 'Set theme']);

  await palette.moveToNextItem();
  expect(await palette.getActiveItemLabel()).toBe('Set theme');
  expect(await palette.isActiveItemAnnounced()).toBe(true);

  await palette.runActiveItem();
});
NameDescriptionTypeDefault
open() / close() / pressTrigger() / pressHotkey() / clickBackdrop()Ways in and out.Promise<void>—
setQuery(text) / getQuery() / getPlaceholder()The search field.Promise<…>—
getItems(filters?) / getItemLabels() / runItem(filters) / getGroups() / getGroupTitles()Filtered results and their groups.Promise<…>—
getActiveItem() / getActiveItemLabel() / getActiveItemIndex() / moveToNextItem() / moveToPreviousItem() / moveToFirstItem() / moveToLastItem() / runActiveItem()The highlighted row and the arrow keys.Promise<…>—
isActiveItemAnnounced() / isSearchWiredToList()Whether aria-activedescendant points at the highlighted row.Promise<boolean>—
getRole() / isModal() / getLabel() / getEmptyText() / focus() / blur() / isSearchInputFocused()Shape, state and focus.Promise<…>—

Cascader

ngwr/cascader/testing

it('picks a path', async () => {
  const cascader = await loader.getHarness(WrCascaderHarness);

  await cascader.open();
  expect(await cascader.getColumnCount()).toBe(1);

  await cascader.selectPath(['Europe', 'Portugal', 'Lisbon']);

  expect(await cascader.getValueText()).toBe('Europe / Portugal / Lisbon');
  expect(await cascader.isOpen()).toBe(false);
});
NameDescriptionTypeDefault
getColumns() / getColumnCount() / getColumn(i) / getColumnLabels()One column per open level.Promise<…>—
selectPath(labels) / getOption(path) / getActiveTrail()selectPath opens each level in turn.Promise<…>—
open() / close() / clickTrigger() / clear() / focus() / blur() / isFocused()The trigger.Promise<…>—
getValueText() / getPlaceholder() / getAccessibleName() / isDisabled() / isOpen()What the trigger shows.Promise<…>—
getPopupRole() / getPanelRole() / getPanelId() / isPanelWiredToTrigger()The trigger-to-panel link.Promise<…>—

Tree

ngwr/tree/testing

it('expands and selects', async () => {
  const tree = await loader.getHarness(WrTreeHarness);

  const root = await tree.getNode({ label: 'src' });
  expect(await root.isExpandable()).toBe(true);
  await root.expand();

  expect(await tree.getNodeLabels()).toEqual(['src', 'app', 'main.ts', 'README.md']);

  await tree.selectNode({ label: 'main.ts' });
  expect(await tree.getSelectedLabels()).toEqual(['main.ts']);
});

it('announces the hierarchy', async () => {
  const tree = await loader.getHarness(WrTreeHarness);
  const app = await tree.getNode({ label: 'app' });

  expect(await app.getLevel()).toBe(2);
  // Per sibling group: 'app' and 'main.ts' are the children of 'src'.
  expect(await app.getSetSize()).toBe(2);
  expect(await app.getPosInSet()).toBe(1);
});
NameDescriptionTypeDefault
getNodes(filters?) / getNodeLabels() / getNode(filters)Visible nodes; a window when virtual.Promise<…>—
selectNode(filters) / getSelectedLabels() / expandAll() / clear()Selection and expansion.Promise<…>—
focusNext() / focusPrevious() / focusFirst() / focusLast() / expandActive() / collapseActive() / selectActive() / getActiveNodeLabel()The keyboard walk, on the active node.Promise<…>—
isOverlay() / isOpen() / open() / close() / getValueText() / getChipLabels() / getOverflowText() / removeChip(label)For a tree used as a picker.Promise<…>—
getRole() / isMultiple() / getSelectionMode() / isVirtual() / isDisabled()Shape and mode.Promise<…>—
Node: getLabel() / getLevel() / getPosInSet() / getSetSize() / getIndex() / isExpandable() / isExpanded() / expand() / collapse() / isSelected() / isDisabled() / isActive() / click() / ctrlClick()WrTreeNodeHarness. Set size and position are per sibling group.Promise<…>—

Graph

ngwr/graph/testing

it('reads the structure, not the drawing', async () => {
  const graph = await loader.getHarness(WrGraphHarness.with({ ariaLabel: 'Team structure' }));

  expect(await graph.getEdges()).toEqual([
    { from: 'lead', to: 'design' },
    { from: 'lead', to: 'build' },
  ]);

  const design = await graph.getNode({ label: 'Design' });
  expect(await design.getText()).toBe('Design');
  expect(await design.getRelationText()).toBe('Parents: Lead');
});
NameDescriptionTypeDefault
getAccessibleName() / getNodes(filters?) / getNode(filters)Nodes in reading order. Filters: label. Graph filters: ariaLabel, nodeLabel.Promise<…>—
getEdges(){ from, to } per line, by node id. Throws on a graph with no nodes.Promise<{ from: string; to: string }[]>—
Node: getText() / getRelationText()WrGraphNodeHarness: the drawn text, and the parents and children sentence.Promise<…>—

Mention

ngwr/mention/testing

it('mentions a teammate', async () => {
  const mention = await loader.getHarness(WrMentionHarness);

  await mention.type('hey @ad');
  expect(await mention.isOpen()).toBe(true);
  expect(await mention.getOptionLabels()).toEqual(['Ada Lovelace']);

  await mention.commit();
  expect(await mention.getValue()).toBe('hey @ada ');
});
NameDescriptionTypeDefault
type(text) / setValue(text) / getValue() / clear()Only type() opens the panel.Promise<…>—
getOptions(filters?) / getOptionLabels() / pick(filters)Suggestions, up to maxResults.Promise<…>—
nextOption() / previousOption() / getActiveOptionLabel() / getActiveOptionIndex() / getActiveOptionId() / commit() / commitWithTab()The arrow walk and the two commit keys.Promise<…>—
dismiss() / blur() / isOpen()Escape closes without picking.Promise<…>—
getAutocomplete() / getPopupRole() / getListboxRole() / getListboxLabel() / getStatusMessage()Combobox ARIA while the panel is open.Promise<…>—

Editor

ngwr/editor/testing

// jsdom has no Range geometry. Delete these again in afterEach.
beforeEach(() => {
  Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
  Range.prototype.getBoundingClientRect = () => new DOMRect();
  document.elementFromPoint = () => null;
});

it('writes through the editor, and reads the value from the model', async () => {
  const body = await loader.getHarness(WrEditorHarness.with({ label: 'Body' }));

  await body.setText('Release notes');
  await body.selectAll();
  await (await body.getTool('Bold')).click();

  expect(await body.getText()).toBe('Release notes');
  expect(await body.isToolPressed('Bold')).toBe(true);
  expect(fixture.componentInstance.body()).toEqual({
    type: 'doc',
    content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Release notes', marks: [{ type: 'strong' }] }] }],
  });

  await body.setLink('javascript:alert(1)');
  expect(await body.getLinkError()).toBe('This address cannot be used as a link.');
});
NameDescriptionTypeDefault
getText() / getLabel() / getPlaceholder() / isMounted()What the surface draws; read the value from your model. Filters: label, text, disabled, readonly.Promise<…>—
setText(text) / paste(text) / pasteHtml(html, text?) / selectAll() / pressKey(key, modifiers?)Edits. pressKey refuses a printable key with no modifier.Promise<void>—
getTool(label) / getToolLabels() / isToolPressed(label) / getToolShortcuts(label) / getToolbarTabStop()Toolbar tools; getTool returns a WrButtonHarness.Promise<…>—
openLinkPanel() / setLink(url) / removeLink() / getLinkError() / isLinkPanelOpen()The link panel. A refused URL leaves it open with an error.Promise<…>—

Without the Range stub, an edit never reaches the model.

Form field

ngwr/form/testing

it('shows the message the rule carries', async () => {
  const field = await loader.getHarness(WrFormFieldHarness);
  const email = await loader.getHarness(WrInputHarness);

  await email.setValue('nope');
  await email.blur();

  // From the rule: email(path.email, { message: 'Enter a valid email address.' })
  expect(await field.getErrorText('email')).toBe('Enter a valid email address.');
  expect(await field.isInvalid()).toBe(true);

  expect(await field.getAnnouncedDescription()).toBe(await field.getErrorText('email'));
  expect(await field.isLabelLinkedToControl()).toBe(true);
});
NameDescriptionTypeDefault
getErrors() / getErrorTexts() / getErrorText(key)Shown messages by key. getErrorText throws when none shows.Promise<…>—
getSuppressedErrorKeys() / hasEmptyErrorBlock()Keys in error with no copy to show.Promise<…>—
getLabel() / isRequired() / isOptional() / getHint()Label and hint.Promise<…>—
isLabelLinkedToControl() / getLabelFor() / getControlId()The <label for> link to the control.Promise<…>—
getDescribedByIds() / getAnnouncedDescription()What aria-describedby reads out.Promise<…>—
isInvalid() / isControlInvalid() / focusControl() / blurControl()The field's painted state, and the control's aria-invalid.Promise<…>—
WrFormItemHarness: getLabel() / isInvalid() / getErrorTexts()The same for <wr-form-item>.Promise<…>—

Tabs

ngwr/tabs/testing

it('switches tabs', async () => {
  const tabs = await loader.getHarness(WrTabsHarness);

  expect(await tabs.getTabLabels()).toEqual(['Overview', 'Details', 'Locked']);
  expect(await tabs.getSelectedLabel()).toBe('Overview');

  await tabs.select({ label: 'Details' });
  expect(await tabs.getSelectedLabel()).toBe('Details');

  const details = (await tabs.getTabs({ label: 'Details' }))[0];
  expect(await details.isPanelBound()).toBe(true);
});

it('walks focus without moving the selection', async () => {
  const tabs = await loader.getHarness(WrTabsHarness);

  await tabs.focusTabStop();
  await tabs.pressArrowRight();

  expect(await tabs.getFocusedLabel()).toBe('Details');
  expect(await tabs.getSelectedLabel()).toBe('Overview');
});
NameDescriptionTypeDefault
getTabLabels() / getTabs(filters?) / getSelectedLabel() / select(filters) / selectByIndex(i)select() throws if the tab does not become selected. Filters: label, selected, disabled.Promise<…>—
getTabStopLabels() / isRoving() / getFocusedLabel() / focusTabStop()The roving tab stop, separate from the selection.Promise<…>—
pressArrowRight() / pressArrowLeft() / pressHome() / pressEnd()Arrows mirror in RTL; Home and End do not.Promise<void>—
isRouterMode() / getRole() / getOrientation() / getSize() / getFades()Shape and edge fades.Promise<…>—
Tab: getLabel() / isSelected() / isDisabled() / isTabStop() / isLink() / getHref() / getPanelId() / isPanelBound() / getPanelText()WrTabHarness. isPanelBound() checks the tab and panel point at each other.Promise<…>—

Stepper

ngwr/stepper/testing

it('walks the steps', async () => {
  const stepper = await loader.getHarness(WrStepperHarness);

  expect(await stepper.getStepLabels()).toEqual(['Cart', 'Address', 'Payment']);
  expect(await stepper.getActiveLabel()).toBe('Cart');

  await stepper.next();
  expect(await stepper.getActiveLabel()).toBe('Address');
  expect(await stepper.getCompletedLabels()).toEqual(['Cart']);

  // A linear stepper refuses a jump.
  expect(await stepper.canGoTo(2)).toBe(false);
  await expect(stepper.goTo(2)).rejects.toThrow();
});
NameDescriptionTypeDefault
getStepLabels() / getSteps(filters?) / getActiveLabel() / getActiveIndex() / getCompletedLabels()Steps and progress. Filters: label, active, completed, disabled, reachable, optional.Promise<…>—
next() / previous() / goTo(i) / goToLabel(label) / canGoTo(i)In a linear stepper, goTo() throws on a jump.Promise<…>—
getActiveStepText() / getStepTexts()Step content.Promise<…>—
isLinear() / getOrientation() / isResponsive() / getListRole() / getTabStopLabels() / getFocusedLabel()Shape and tab stop.Promise<…>—
Step: getLabel() / getDescription() / isOptional() / getAccessibleName() / isActive() / isCompleted() / isReachable() / select()WrStepHarness. select() respects isReachable().Promise<…>—

Carousel

ngwr/carousel/testing

NameDescriptionTypeDefault
getSlideCount() / getSlideTexts() / getActiveIndex() / getActiveSlideText()What is shown.Promise<…>—
next() / previous() / goTo(i)Next stays next in RTL. There is no drag method.Promise<void>—
hasDots() / getDotCount() / getDotLabels() / hasArrows() / getPreviousLabel() / getNextLabel()Dots, arrows and their labels.Promise<…>—
hover() / mouseAway()Pauses and resumes autoplay.Promise<void>—
getRole() / getRoleDescription() / getSlideRoleDescriptions() / getAccessibleName() / getTrackOffsetPercent()Roles, name and track offset.Promise<…>—

Pagination

ngwr/pagination/testing

it('pages through', async () => {
  const pager = await loader.getHarness(WrPaginationHarness);

  expect(await pager.getCurrentPage()).toBe(1);
  expect(await pager.isPreviousDisabled()).toBe(true);

  expect(await pager.getPages()).toEqual([1, 2, 3, 4, 5, 10]);
  expect(await pager.getStrip()).toEqual([1, 2, 3, 4, 5, '…', 10]);

  await pager.goToPage(10);
  expect(await pager.isNextDisabled()).toBe(true);

  await pager.setPageSize(50);
  expect(await pager.getPageSize()).toBe(50);
});
NameDescriptionTypeDefault
getPages() / getStrip() / getCurrentPage() / getTotalPages()getPages() is numbers only; getStrip() includes the '…' gaps.Promise<…>—
goToPage(n) / goToFirst() / goToLast() / next() / previous()Moving between pages.Promise<void>—
isNextDisabled() / isPreviousDisabled() / isDisabled()The ends, and the whole control.Promise<boolean>—
hasPageSizeChanger() / getPageSizeSelect() / getPageSize() / setPageSize(n)getPageSizeSelect() returns a WrSelectHarness.Promise<…>—
hasTotal() / getTotalText() / getLabel() / getSize() / isResponsive()Copy and chrome.Promise<…>—

Segmented

ngwr/segmented/testing

NameDescriptionTypeDefault
getOptionLabels() / getOptions(filters?) / getSelectedLabel() / getSelectedIndex() / select(filters) / selectAt(i)Options and the choice. Filters: label, selected, disabled.Promise<…>—
getTabStopLabels() / getFocusedLabel()The roving tab stop.Promise<…>—
getThumbIndex() / getThumbCount() / isThumbVisible() / isThumbTransitionEnabled()The sliding thumb.Promise<…>—
getRole() / getAccessibleName() / getSize() / isDisabled()Shape and state.Promise<…>—

Collapse

ngwr/collapse/testing

it('opens one panel at a time', async () => {
  const group = await loader.getHarness(WrCollapseGroupHarness);

  await group.openPanel({ title: 'Shipping' });
  expect(await group.getOpenTitles()).toEqual(['Shipping']);

  // Accordion mode: opening the next one closes the first.
  await group.openPanel({ title: 'Payment' });
  expect(await group.getOpenTitles()).toEqual(['Payment']);

  const shipping = await group.getPanel({ title: 'Shipping' });
  expect(await shipping.isOpen()).toBe(false);
  expect(await shipping.isRegionBound()).toBe(true);
});
NameDescriptionTypeDefault
Group: getPanelTitles() / getPanels(filters?) / getPanel(filters) / getPanelAt(i) / getOpenTitles()WrCollapseGroupHarness. Filters: title, open, disabled.Promise<…>—
Group: openPanel(filters) / closePanel(filters) / closeAll() / getFocusedTitle()In accordion mode, opening one closes the rest.Promise<…>—
Panel: isOpen() / open() / close() / toggle() / getContentText()WrCollapseHarness. Open state comes from aria-expanded.Promise<…>—
Panel: getRegionId() / isRegionBound() / isContentHidden()The header and region point at each other.Promise<…>—

Transfer

ngwr/transfer/testing

it('moves a row across', async () => {
  const transfer = await loader.getHarness(WrTransferHarness);
  const source = await transfer.getPane('source');

  await (await source.getItem({ label: 'Write' })).check();
  expect(await transfer.canMoveTo('target')).toBe(true);

  await transfer.moveTo('target');
  expect(await (await transfer.getPane('target')).getItemLabels()).toEqual(['Write']);

  await expect(transfer.moveTo('target')).rejects.toThrow(/nothing staged/);
});
NameDescriptionTypeDefault
getPane(side)'source' or 'target'.Promise<WrTransferPaneHarness>—
canMoveTo(side) / moveTo(side) / moveAllTo(side) / getMoveLabel(side)moveTo() throws when nothing is staged.Promise<…>—
Pane: getItemLabels() / getItems(filters?) / getItem(filters) / getCheckedLabels() / getEmptyText()The rows shown. Filters: label, checked, disabled.Promise<…>—
Pane: toggleSelectAll() / checkAll() / uncheckAll() / isAllChecked() / isPartiallyChecked() / isSelectAllDisabled()Select-all covers the rows shown.Promise<…>—
Pane: hasSearch() / search(query) / getSearchValue() / getCountText() / getTitle() / getListRole()search() throws without searchable.Promise<…>—
Item: getLabel() / isChecked() / isDisabled() / toggle() / check() / uncheck()WrTransferItemHarness. A disabled row refuses.Promise<…>—

Accessible names for end-to-end tests

The names ngwr renders itself, for role and name locators.

ElementDefault nameOverride
Pagination landmarkPagination[label] · pagination.label
Pagination pageGo to page 3pagination.goToPage
Pagination previous / nextPrevious page · Next page[prevLabel] / [nextLabel] · pagination.prev / .next
Pagination size changerItems per pagepagination.itemsPerPage
Table sort buttonSort Name (the column title)[sortLabel] · table.sort
Table row checkboxSelect row Ada: since v15 the action word, then the row's label cell (the first column, or the tree column in tree mode) [selectRowLabel] · table.selectRow, the action word only
Table select-all checkboxSelect all rows[selectAllLabel] · table.selectAll
Table detail / tree toggleToggle row details Ada · Toggle child rows Engineering[expandRowLabel] · table.expandRow / [toggleRowLabel] · table.toggleRow
Table group checkbox / toggleSelect group admin · Toggle group admin 2: the group's label, then its row count while the badge shows [selectGroupLabel] · table.selectGroup / [toggleGroupLabel] · table.toggleGroup
Table loading overlayLoading…[loadingLabel] · table.loading
Dialog ✕Close dialogcloseLabel · dialog.close
Drawer ✕Close drawercloseLabel · drawer.close
Select clear ✕Clear selectionselect.clearSelection
Select chip removeRemove Berlinselect.removeItem
  • A [sortLabel] gives every sort button the same name: scope the query to its header cell.
  • An aria-label on a component's host names nothing: bind its ariaLabel input.

Naming a table

<wr-table> takes no name, so wrap it in a named region.

<!-- The visible heading names the region. -->
<section role="region" aria-labelledby="open-orders-heading">
  <h2 id="open-orders-heading">Open orders</h2>
  <wr-table [columns]="cols" [items]="orders()" />
</section>

<!-- No heading? Name the region directly. -->
<section role="region" aria-label="Archived orders">
  <wr-table [columns]="cols" [items]="archived()" />
</section>

Running axe over your own app

A closed select, cascader or overlay tree reports aria-valid-attr-value as incomplete: filter those nodes, not the rule.

const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);

// Only if your gate also reads `incomplete`: drop the closed-combobox nodes.
const forReview = results.incomplete.filter(
  r =>
    r.id !== 'aria-valid-attr-value' ||
    r.nodes.some(n => !/aria-controls/.test(n.html) || !/aria-expanded="false"/.test(n.html)),
);
expect(forReview).toEqual([]);

Open an overlay in the test before you scan it.

Your own harness

Wrap your own components the same way, with the CDK base class.

import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';

export class MyWidgetHarness extends ComponentHarness {
  static hostSelector = 'my-widget';

  static with(options: { title?: string } = {}) {
    return new HarnessPredicate(MyWidgetHarness, options).addOption('title', options.title, (harness, title) =>
      HarnessPredicate.stringMatches(harness.getTitle(), title)
    );
  }

  async getTitle(): Promise<string> {
    return (await this.locatorFor('.my-widget__title')()).text();
  }
}

Your own controls

Check a Signal Forms control you write.

See also