Opinionated testing package that combines and configures testing libraries to minimize ceremony when writing tests
npx @tessl/cli install tessl/npm-open-wc--testing@4.0.0@open-wc/testing is an opinionated testing package that combines and configures multiple testing libraries to minimize ceremony when writing tests for web components and modern JavaScript applications. It provides Chai assertions, fixture management utilities, and testing helpers in a single, cohesive package. The package offers two import modes: standard (with automatic chai plugin registration) and pure (manual setup required).
npm install --save-dev @open-wc/testingRecommended approach (pure imports without automatic plugin registration):
import { expect, fixture, html, chai, assert, should } from '@open-wc/testing/pure';Standard imports (with automatic chai plugin registration - requires built plugins):
import { expect, fixture, html, chai, assert, should } from '@open-wc/testing';Note: The main import may fail at runtime if chai plugins haven't been built. Use the pure import for reliability.
CommonJS (if using pre-ES6 environment):
const { expect, fixture, html, chai, assert, should } = require('@open-wc/testing/pure');import { expect, fixture, html, elementUpdated } from '@open-wc/testing/pure';
describe('MyElement', () => {
it('renders correctly', async () => {
// Create a fixture with lit-html template
const el = await fixture(html`<my-element name="test"></my-element>`);
// Test DOM structure with semantic comparison (requires main import for plugin)
// expect(el).dom.to.equal('<my-element name="test"></my-element>');
// Test accessibility (requires main import for plugin)
// await expect(el).to.be.accessible();
// Basic DOM testing (always works)
expect(el.tagName.toLowerCase()).to.equal('my-element');
expect(el.getAttribute('name')).to.equal('test');
// Test element behavior
el.name = 'updated';
await elementUpdated(el);
expect(el.textContent).to.include('updated');
});
it('handles events', async () => {
const el = await fixture(html`<button>Click me</button>`);
let clicked = false;
el.addEventListener('click', () => clicked = true);
el.click();
expect(clicked).to.be.true;
});
});@open-wc/testing is built around several key components:
@open-wc/testing): Automatic chai plugin registration, requires built plugins@open-wc/testing/pure): No automatic setup, manual plugin configuration neededCore assertion library with pre-configured plugins for comprehensive testing capabilities.
const chai: typeof Chai;
const expect: typeof Chai.expect;
const assert: typeof Chai.assert;
const should: typeof Chai.should;Usage Examples:
import { expect, assert, should } from '@open-wc/testing/pure';
// Expect style
expect(value).to.equal(42);
expect(element).to.have.class('active');
// Assert style
assert.equal(value, 42);
assert.isAccessible(element);
// Should style
value.should.equal(42);
element.should.have.class('active');Create and manage DOM fixtures for testing components in isolation.
/**
* Asynchronously renders template and puts it in DOM, awaiting element updates
*/
function fixture<T extends Element>(
template: LitHTMLRenderable,
options?: FixtureOptions
): Promise<T>;
/**
* Synchronously renders template and puts it in DOM
*/
function fixtureSync<T extends Element>(
template: LitHTMLRenderable,
options?: FixtureOptions
): T;
/**
* Specifically handles lit-html templates for async fixture setup
*/
function litFixture<T extends Element>(
template: LitHTMLRenderable,
options?: FixtureOptions
): Promise<T>;
/**
* Synchronous version of litFixture
*/
function litFixtureSync<T extends Element>(
template: LitHTMLRenderable,
options?: FixtureOptions
): T;
/**
* Cleans up all defined fixtures by removing wrapper nodes from DOM
*/
function fixtureCleanup(): void;
interface FixtureOptions {
render?: Function;
parentNode?: Element;
scopedElements?: ScopedElementsMap;
}
type LitHTMLRenderable =
| TemplateResult
| TemplateResult[]
| Node | Node[]
| string | string[]
| number | number[]
| boolean | boolean[];Usage Examples:
import { fixture, fixtureSync, fixtureCleanup, html } from '@open-wc/testing/pure';
// Async fixture creation
const el = await fixture(html`<div>Hello World</div>`);
const el2 = await fixture('<span>Text content</span>');
// Sync fixture creation
const syncEl = fixtureSync(html`<p>Immediate render</p>`);
// Custom parent node
const customEl = await fixture(html`<div>Child</div>`, {
parentNode: document.getElementById('container')
});
// Clean up all fixtures (usually in afterEach)
afterEach(() => {
fixtureCleanup();
});Create lit-html templates for fixture rendering and testing.
/**
* Creates lit-html templates for testing (tagged template literal)
*/
const html: (strings: TemplateStringsArray, ...values: any[]) => TemplateResult;
/**
* Allows dynamic tag names in lit-html templates (for testing only)
*/
function unsafeStatic(value: string): StaticValue;Usage Examples:
import { fixture, html, unsafeStatic } from '@open-wc/testing/pure';
// Basic template
const el = await fixture(html`<div class="test">Content</div>`);
// Template with expressions
const name = 'Alice';
const el2 = await fixture(html`<span>Hello ${name}</span>`);
// Dynamic tag names (use carefully)
const tagName = unsafeStatic('my-element');
const el3 = await fixture(html`<${tagName} prop="value"></${tagName}>`);Manage element updates, custom element registration, and lifecycle events.
/**
* Awaits element update completion (supports Lit, Stencil, generic elements)
*/
function elementUpdated<T extends Element>(el: T): Promise<T>;
/**
* Registers new custom element with auto-generated unique name
*/
function defineCE<T extends HTMLElement>(klass: Constructor<T>): string;
type Constructor<T = {}> = new (...args: any[]) => T;Usage Examples:
import { fixture, elementUpdated, defineCE, html } from '@open-wc/testing/pure';
// Wait for element updates
const el = await fixture(html`<my-element></my-element>`);
el.someProperty = 'new value';
await elementUpdated(el); // Waits for Lit/Stencil updates
// Register custom element for testing
class TestElement extends HTMLElement {
connectedCallback() {
this.textContent = 'Test Element';
}
}
const tagName = defineCE(TestElement); // Returns 'test-0', 'test-1', etc.
const el2 = await fixture(`<${tagName}></${tagName}>`);Utilities for testing event-driven behavior and user interactions.
/**
* Listens for one event and resolves with the event object
*/
function oneEvent(
eventTarget: EventTarget,
eventName: string
): Promise<Event>;
/**
* Like oneEvent but automatically calls preventDefault() on the event
*/
function oneDefaultPreventedEvent(
eventTarget: EventTarget,
eventName: string
): Promise<Event>;
/**
* Focuses element with IE11 compatibility workarounds
*/
function triggerFocusFor(element: HTMLElement): Promise<void>;
/**
* Blurs element with IE11 compatibility workarounds
*/
function triggerBlurFor(element: HTMLElement): Promise<void>;Usage Examples:
import { fixture, oneEvent, oneDefaultPreventedEvent, triggerFocusFor } from '@open-wc/testing/pure';
// Listen for custom events
const el = await fixture(html`<my-button></my-button>`);
const eventPromise = oneEvent(el, 'my-custom-event');
el.dispatchEvent(new CustomEvent('my-custom-event', { detail: 'data' }));
const event = await eventPromise;
console.log(event.detail); // 'data'
// Prevent default behavior
const link = await fixture(html`<a href="/test">Link</a>`);
const clickPromise = oneDefaultPreventedEvent(link, 'click');
link.click();
await clickPromise; // Event was prevented
// Focus management
const input = await fixture(html`<input type="text">`);
await triggerFocusFor(input);
expect(document.activeElement).to.equal(input);Control timing and wait for conditions during tests.
/**
* Resolves after specified milliseconds using setTimeout
*/
function aTimeout(ms: number): Promise<void>;
/**
* Resolves after requestAnimationFrame
*/
function nextFrame(): Promise<void>;
/**
* Waits until predicate returns truthy value, polling at intervals
*/
function waitUntil(
predicate: () => unknown | Promise<unknown>,
message?: string,
options?: { interval?: number; timeout?: number }
): Promise<void>;Usage Examples:
import { aTimeout, nextFrame, waitUntil } from '@open-wc/testing/pure';
// Simple timeout
await aTimeout(100); // Wait 100ms
// Wait for animation frame
await nextFrame(); // Wait for next render cycle
// Wait for condition with polling
const el = await fixture(html`<div></div>`);
setTimeout(() => el.classList.add('loaded'), 50);
await waitUntil(
() => el.classList.contains('loaded'),
'Element should be loaded',
{ interval: 10, timeout: 1000 }
);Utilities for handling browser-specific behavior and compatibility.
/**
* Detects Internet Explorer browser
*/
function isIE(): boolean;Usage Examples:
import { isIE } from '@open-wc/testing/pure';
if (isIE()) {
// Skip test or use IE-specific behavior
console.log('Running in Internet Explorer');
}Advanced DOM comparison that ignores whitespace, comments, and other non-semantic differences.
New Chai Assertions:
// Available on any DOM element via expect()
interface Assertion {
dom: Assertion; // Compare full DOM trees semantically
lightDom: Assertion; // Compare light DOM only
}
// Additional methods on dom/lightDom assertions
interface Assertion {
equal(expected: string): Assertion; // Semantic equality
equalSnapshot(name?: string): Assertion; // Snapshot testing
}Usage Examples:
import { expect, fixture } from '@open-wc/testing/pure';
// Semantic DOM comparison ignores formatting
const el = await fixture(`<div><!-- comment --><h1> Hello </h1> </div>`);
expect(el).dom.to.equal('<div><h1>Hello</h1></div>');
// Light DOM comparison (excludes shadow DOM)
expect(el).lightDom.to.equal('<h1>Hello</h1>');
// Snapshot testing (when supported by test runner)
expect(el).dom.to.equalSnapshot();
expect(el).dom.to.equalSnapshot('custom-name');Automated accessibility testing using axe-core integration.
New Chai Assertions:
interface Assertion {
accessible(options?: A11yOptions): Promise<Assertion>;
}
interface A11yOptions {
ignoredRules?: string[]; // Skip specific axe rules
done?: Function; // Callback for async testing patterns
}
// Also available on assert interface
declare namespace assert {
function isAccessible(
element: Element,
options?: A11yOptions
): Promise<void>;
}Usage Examples:
import { expect, assert, fixture, html } from '@open-wc/testing/pure';
// Basic accessibility testing
const button = await fixture(html`<button>Click me</button>`);
await expect(button).to.be.accessible();
// Ignore specific rules
const problematicEl = await fixture(html`<div aria-labelledby="missing-id"></div>`);
await expect(problematicEl).to.be.accessible({
ignoredRules: ['aria-allowed-attr']
});
// Assert style
const form = await fixture(html`<form><label>Name <input></label></form>`);
await assert.isAccessible(form);
// Negation for expected failures
const badEl = await fixture(html`<div aria-labelledby="missing"></div>`);
await expect(badEl).not.to.be.accessible();Available Plugins (functional with main import):
@open-wc/semantic-dom-diff - Provides .dom.to.equal() and .lightDom.to.equal() assertionschai-a11y-axe - Provides .accessible() accessibility testing assertionsCurrently Non-functional Plugins:
The package references chai-dom and sinon-chai plugins but they require a build step that may not be available. If these plugins are working in your environment, they would provide:
chai-dom - DOM assertions like .to.have.class(), .to.have.attribute()sinon-chai - Spy/stub assertions like .to.have.callCount()Recommendation: Use the pure import (@open-wc/testing/pure) and manually configure additional chai plugins as needed to avoid runtime import errors.
// Re-exported from @esm-bundle/chai
type Chai = typeof import('chai');
// Fixture configuration
interface FixtureOptions {
render?: Function;
parentNode?: Element;
scopedElements?: ScopedElementsMap;
}
// Template types for fixture creation
type LitHTMLRenderable =
| TemplateResult
| TemplateResult[]
| Node | Node[]
| string | string[]
| number | number[]
| boolean | boolean[];
// Event handling types
type OneEventFn = <TEvent extends Event = CustomEvent>(
eventTarget: EventTarget,
eventName: string
) => Promise<TEvent>;
// Element constructor type
type Constructor<T = {}> = new (...args: any[]) => T;
// Accessibility testing options
interface A11yOptions {
ignoredRules?: string[];
done?: Function;
}
// Timing utility options
interface WaitUntilOptions {
interval?: number; // Polling interval in ms (default: 50)
timeout?: number; // Total timeout in ms (default: 1000)
}For maximum reliability and compatibility:
@open-wc/testing/pure to avoid potential runtime issuesExample minimal setup:
import { expect, fixture, html, elementUpdated } from '@open-wc/testing/pure';
describe('Component Tests', () => {
it('creates fixtures and tests elements', async () => {
const el = await fixture(html`<div class="test">Hello</div>`);
expect(el.textContent).to.equal('Hello');
expect(el.className).to.equal('test');
});
});