npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dom-harness

v1.1.2

Published

DOM component test harness library inspired by Angular CDK's ComponentHarness

Downloads

250

Readme

dom-harness

npm version npm downloads TypeScript Bundle Size

A lightweight DOM component test harness library inspired by Angular CDK's ComponentHarness. It provides a structured way to interact with rendered DOM components in tests, hiding selector details behind a clean API and making tests more readable and maintainable.

New here? Read the Getting Started guide for a step-by-step tutorial.

Why use test harnesses?

Tests that query the DOM directly are fragile — selectors are scattered across test files, interaction boilerplate is duplicated, and any markup change ripples through every test that touches the component. A harness encapsulates all of that in one place so tests read like user interactions, not DOM traversals.

Without harnesses — selectors and interactions leak into every test:

it('should show welcome message after login', async () => {
  render(<LoginForm />);
  const user = userEvent.setup();

  const form = document.querySelector('[data-testid="login-form"]')!;
  const inputs = form.querySelectorAll('[data-testid="text-input"]');
  const username = [...inputs].find(el => (el as HTMLInputElement).name === 'username')! as HTMLInputElement;
  const password = [...inputs].find(el => (el as HTMLInputElement).name === 'password')! as HTMLInputElement;
  const button = form.querySelector('[data-testid="button"]')! as HTMLButtonElement;

  await user.type(username, 'testuser');
  await user.type(password, 'password123');
  await user.click(button);

  expect(form.querySelector('[data-testid="welcome"]')!.textContent).toBe('Welcome, testuser!');
});

With harnesses — the same test, readable and resilient to markup changes:

it('should show welcome message after login', async () => {
  render(<LoginForm />);
  const form = LoginFormHarness.first();

  await form.usernameInput.type('testuser');
  await form.passwordInput.type('password123');
  await form.submitButton.click();

  expect(form.welcomeText()).toBe('Welcome, testuser!');
});

Installation

npm install dom-harness

Peer dependencies

| Package | Version | |---|---| | @testing-library/user-event | >=14.0.0 |

Quick start

1. Create leaf harnesses

import { DomHarness } from 'dom-harness';

export class TextInputHarness extends DomHarness {
  static testid = 'text-input';
  get input() { return this.root as HTMLInputElement; }
  static byName(name: string, container?: Element) { return this.find(h => h.name() === name, container); }
  async type(value: string) { await this.user.type(this.input, value); }
  value() { return this.input.value; }
  name() { return this.input.name; }
}

export class ButtonHarness extends DomHarness {
  static testid = 'button';
  get button() { return this.root as HTMLButtonElement; }
  async click() { await this.user.click(this.button); }
  text() { return this.button.textContent ?? ''; }
}

2. Compose into a form harness

import { DomHarness } from 'dom-harness';
import { TextInputHarness } from '../text-input/TextInputHarness';
import { ButtonHarness } from '../button/ButtonHarness';

export class LoginFormHarness extends DomHarness {
  static testid = 'login-form';

  get usernameInput() { return TextInputHarness.byName('username', this.root); }
  get passwordInput() { return TextInputHarness.byName('password', this.root); }
  get submitButton() { return ButtonHarness.first(this.root); }
  get welcomeMessage() { return this.queryElement('[data-testid="welcome"]', true); }

  welcomeText() { return this.welcomeMessage?.textContent ?? null; }
}

3. Use in a test

import { render } from '@testing-library/react';
import { LoginFormHarness } from './LoginFormHarness';

it('should show welcome message after login', async () => {
  render(<LoginForm />);
  const form = LoginFormHarness.first();

  await form.usernameInput.type('testuser');
  await form.passwordInput.type('password123');
  await form.submitButton.click();

  expect(form.welcomeText()).toBe('Welcome, testuser!');
});

Examples

The examples/ directory contains the same TextInput, Button, and LoginForm components implemented in 6 frameworks. The harness code is identical across all of them — only the component rendering differs.

examples/
  react/     — React 19
  preact/    — Preact 10
  solid/     — Solid 1.9
  vue/       — Vue 3.5
  svelte/    — Svelte 5
  angular/   — Angular 19

Each example runs tests with npm test (vitest).

API reference

Static properties

| Property | Type | Description | |---|---|---| | testid | string \| undefined | Maps to [data-testid="<value>"] selector. | | selector | string \| undefined | Raw CSS selector. Used when testid is not set. |

At least one of testid or selector must be defined on a harness subclass.

Static methods

first(container?: Element): T

Returns a harness instance for the first matching element in the DOM (or within container).

const btn = ButtonHarness.first();

all(container?: Element): T[]

Returns harness instances for all matching elements.

const buttons = ButtonHarness.all();
expect(buttons).toHaveLength(3);

find(matcher: (el: T) => boolean, container?: Element): T

Returns the first harness whose instance satisfies matcher. Throws if no match is found.

const submit = ButtonHarness.find(b => b.text() === 'Submit');

match(textOrRegexp: string | RegExp, getText: (h: T) => string, container?: Element): T

Convenience wrapper around find that matches by text content or regex.

const cancel = ButtonHarness.match('Cancel', b => b.text());
const save = ButtonHarness.match(/save/i, b => b.text());

fromDomElement(root?: Element): T

Wraps an existing DOM element in a harness, bypassing selector lookup.

const el = document.querySelector('.my-button')!;
const btn = ButtonHarness.fromDomElement(el);

Instance properties and methods

root: Element

The underlying DOM element for this harness.

user: UserEvent

A @testing-library/user-event instance for simulating user interactions.

await harness.user.click(harness.root);

queryElement(selector: string): Element

Queries a descendant of root by CSS selector. Throws if no element is found. In practice, most harnesses use this.root.querySelector(...) directly for more control over null handling.

const icon = this.root.querySelector('.icon');

Patterns

Composing harnesses

Harnesses can reference other harnesses for child components. Pass this.root as the container to scope queries to the current component's DOM:

class LoginFormHarness extends DomHarness {
  static testid = 'login-form';

  get usernameInput() { return TextInputHarness.byName('username', this.root); }
  get passwordInput() { return TextInputHarness.byName('password', this.root); }
  get submitButton() { return ButtonHarness.first(this.root); }
}

Convenience finders

Define static methods for common lookups. match works well for text-based matching, while find handles arbitrary predicates:

class ButtonHarness extends DomHarness {
  static selector = 'button';

  static withText(text: string | RegExp) {
    return this.match(text, b => b.text());
  }

  text() { return this.root.textContent ?? ''; }
}

// Usage
const ok = ButtonHarness.withText('OK');
class TextInputHarness extends DomHarness {
  static testid = 'text-input';

  static byName(name: string, container?: Element) {
    return this.find(h => h.name() === name, container);
  }

  name() { return (this.root as HTMLInputElement).name; }
}

// Usage
const email = TextInputHarness.byName('email');

Selector via CSS class

When data-testid is not available, use selector:

class CardHarness extends DomHarness {
  static selector = '.MuiCard-root';
}