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 🙏

© 2025 – Pkg Stats / Ryan Hefner

element-test-harness

v0.0.12

Published

utility for testing custom elements

Readme

Element Test Harness

This is a helper for testing custom elements built using Lit Element. It evolved from the practice of writing wrapper classes for custom elements so that the tests are easy to read and maintain.

For example, imagine a component that implements a calculator like the one built into iOS. Tests might look something like this.

import { html } from "lit";
import { TestHarness } from "@wh-hc-dev/element-test-harness";

import { MyCalculator } from "../src/my-calculator";

class CalculatorHarness extends TestHarness<MyCalculatorElement> {
  static events = ["calculationComplete"];

  static basic() {
    return this.fixture(html`<my-calculator></my-calculator>`);
  }

  static scientific() {
    return this.fixture(html`<my-calculator scientific></my-calculator>`);
  }

  async pressButtons(...buttons) {
    await Array.from(buttons).forEach(async (button) => {
      this.qs(`#${button}`).click();
      await this.updateComplete();
    });
  }

  get display() {
    return this.qs("#display").textContent();
  }
}

it("adds two numbers", async () => {
  const calculator = await CalculatorHarness.basic();

  await calculator.pressButtons(2, "+", 2, "=");
  expect(calculator.display).toEqual("4");
});

it("groups with parentheses", async () => {
  const calculator = await CalculatorHarness.scientific();

  await calculator.pressButtons(7, "*", "(", 5, "+", 2, ")");
  expect(calculator.display).toEqual("70");
});

Set Up

At the top of your test file (before any it() or describe() calls) create a subclass of TestHarness, passing your element as a type parameter.

import { TestHarness } from "@wh-hc-dev/element-test-harness";

import { MyElement } from "../src/my-element";

class MyTestHarness extends TestHarness<MyElement> {
  // intentionally left blank (for now)
}

To get an instance of your test harness, use the async static method, fixture.

const fixture = await MyTestHarness.fixture(
  document.createElement("my-element")
);

In practice, it's helpful to add static methods to your subclass to get fixtures of elements that are configured with certain properties.

class MyTestHarness extends TestHarness<MyElement> {
  static simple() {
    return this.fixture(document.createElement("my-element"));
  }

  static fancy({ color }) {
    return this.fixture(html`<my-element color=${color}></my-element>`);
  }
}

it("can be simple", async () => {
  const simple = await myTestHarness.simple();

  // ...
});

it("can be fancy", async () => {
  const fancy = await myTestHarness.fancy({ color: "hotpink" });

  // ...
});

API

Querying the Shadow DOM

The .qs() method is a shorthand for element.shadowDom.querySelector().

const fixture = await MyTestHarness.fixture();
const button = fixture.qs<HTMLButtonElement>("#save-button");

If no matching element is not found, qs() will throw an error. If you want to test whether an element exists, uses hasElementMatchingSelector()

expect(fixture.hasElementMatchingSelector("#save-button")).toBe(true);

The .qsa() method is a shorthand for element.shadowDom.querySelectorAll(). It returns the list of matching items wrapped in an array, so you can call map(), filter(), find(), etc. (querySelectorAll() returns a NodeList).

const fixture = await MyTestHarness.fixture();
const buttons = fixture.qsa<HTMLButtonElement>("button");
const buttonLabels = buttons.map((button) => button.textContent);

Changing Properties and Awaiting Updates

When properties are updated on a LitElement, the element doesn't rerender immediately. We need to wait for the updateCompete promise.

const fixture = await MyTestHarness.fixture();

expect(fixture.count).toEqual(0);
fixture.qs<HTMLButtonElement>("#increment").click();
expect(fixture.count).toEqual(0);
await fixture.updateComplete;
expect(fixture.count).toEqual(1);

Verifying Events

TestHarness logs events that are dispatched by the element (i.e. events that can be registered with element.addEventListener()).

const fixture = await MyTestHarness.fixture();

const incrementButton = fixture.qs<HTMLButtonElement>("#increment");

increment.click();
increment.click();
increment.click();

expect(fixture.dispatchedEvents().length).toBe(3);
expect(fixture.lastEvent("click").target).toBe(incrementButton);

Note that in order for the test harness to listen for an event the type needs to be declared in the static property, events.

class MyTestHarness extends TestHarness<MyCalculatorElement> {
  static events = ["calculationComplete"];
}

Usage

In practice, you won't often access the methods and properties of TestHarness directly from the tests. Instead, you'll use them to build out your own harness, which is a subclass of TestHarness.

Let's take a look at the calculator harness from the top again in more detail.

class CalculatorHarness extends TestHarness<MyCalculatorElement> {
  // declare the event types used in tests
  static events = ["calculationComplete"];

  /**
   * shortcut for a basic calculator
   */
  static basic() {
    return this.fixture(html`<my-calculator></my-calculator>`);
  }

  /**
   * shortcut for a calculator instance with the "scientific" attribute enabled
   */
  static scientific() {
    return this.fixture(html`<my-calculator scientific></my-calculator>`);
  }

  /**
   * abstracts the act of clicking several buttons in a
   * sequence and waiting for the component to re-render
   */
  async pressButtons(...buttons) {
    await Array.from(buttons).forEach(async (button) => {
      this.qs(`#${button}`).click();
      await this.updateComplete();
    });
  }

  /**
   * shortcut to the contents of the calculator display
   */
  get display() {
    return this.qs("#display").textContent();
  }
}

A well-designed harness encapsulates the grunt work of fiddling with the DOM so that the unit tests themselves are clear and concise.