Testing a Lit component means rendering it into a real (or headless) browser DOM and asserting on its Shadow DOM output and behavior. This lesson covers the standard tooling and patterns.
Testing in a Real Browser Environment
Because Lit components rely on genuine browser APIs (Custom Elements, Shadow DOM), they need to run in an actual browser environment to be tested meaningfully — a Node.js-only DOM simulation like jsdom has historically had gaps in Custom Elements/Shadow DOM support. The Modern Web team's @web/test-runner, paired with @open-wc/testing helpers, is the standard, actively maintained tooling for this, running tests in real (optionally headless) browsers via Playwright or Puppeteer.
The core testing pattern is: render a fresh instance of your component into the test DOM using a fixture() helper, query into its Shadow DOM to assert on rendered output, and simulate user interaction to test behavior.
import { fixture, html, expect } from '@open-wc/testing';
import './greeting-card.js';
describe('greeting-card', () => {
it('renders a greeting with the given name', async () => {
const el = await fixture(html`<greeting-card name="Ada"></greeting-card>`);
const p = el.shadowRoot.querySelector('p');
expect(p.textContent).to.include('Hello, Ada!');
});
});
fixture() renders the element into the live test DOM and automatically waits for its first update to complete before the test continues.
Common Testing Patterns
const el = await fixture(html`<my-el></my-el>`);
await el.updateComplete; // wait after a later property change
el.shadowRoot.querySelector('.foo'); // query rendered output
el.shadowRoot.querySelector('button').click(); // simulate a click
expect(el).to.be.accessible(); // aXe-powered accessibility assertion
fixture() automatically cleans up the rendered element after each test, avoiding test-to-test DOM pollution.
Always await el.updateComplete after changing a property mid-test, before asserting on the resulting DOM.
@open-wc/testing's expect(el).to.be.accessible() runs an automated accessibility audit (via axe-core) directly in your test suite.
Simulate real user events (.click(), dispatching keydown) rather than calling a component's internal handler methods directly, to test the actual user-facing behavior.
Renders a component into the test DOM, waits for first update
el.updateComplete
Promise resolving after a pending update finishes
el.shadowRoot.querySelector(...)
Queries the component's rendered Shadow DOM
expect(el).to.be.accessible()
Automated accessibility check via axe-core
Testing Dispatched Custom Events
To test that a component dispatches an expected custom event, attach a listener before triggering the interaction, then assert on the captured event's detail once the interaction completes.
Wrapping the listener in a Promise lets the test await the event cleanly rather than using arbitrary timeouts.
Testing Slotted Content and Accessibility
Slotted content can be tested by rendering a fixture with real light DOM children and asserting on the resulting rendered layout; automated accessibility testing catches missing labels, incorrect ARIA usage, and insufficient color contrast without manual screen reader testing for every single change.
Testing Lit components with a Node.js-only DOM simulation lacking full Custom Elements/Shadow DOM support, leading to unreliable results.
Forgetting to await el.updateComplete after a mid-test property change, then asserting on stale DOM before the update actually applied.
Calling a component's private handler methods directly in tests instead of simulating real user events, testing implementation rather than behavior.
Skipping automated accessibility checks entirely, missing an easy, low-effort way to catch common a11y regressions in CI.
Key Takeaways
Lit components should be tested in a real (or headless) browser environment, typically via @web/test-runner.
@open-wc/testing's fixture() helper renders a component and waits for its first update automatically.
Test dispatched custom events by attaching a listener before the interaction and asserting on the captured detail.
Automated accessibility assertions (to.be.accessible()) are a low-effort, high-value addition to a Lit test suite.
Pro Tip
Add expect(el).to.be.accessible() to your test template/boilerplate for every new component from day one — making automated accessibility checking the default, rather than an afterthought, catches a meaningful share of a11y regressions before they ever reach code review.
You've completed the advanced topics series. Next, review Lit best practices as a consolidated reference.