Skip to content

Testing

Test behavior at its owner boundary: the source, the core, then the rendered controls.

Use ordinary assertions.

For a browser-like Vitest host:

Terminal
pnpm add -D vitest jsdom @testing-library/dom
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { environment: 'jsdom' },
});

Use the same JSX settings as the application.

source.test.ts
import { createActor, createMachine } from "xstate";
import { expect, it } from "vitest";
it("toggles in the source without a renderer", () => {
const toggleMachine = createMachine({
initial: "off",
states: {
off: { on: { TOGGLE: "on" } },
on: { on: { TOGGLE: "off" } },
},
});
const source = createActor(toggleMachine).start();
try {
source.send({ type: "TOGGLE" });
expect(source.getSnapshot().matches("on")).toBe(true);
source.send({ type: "TOGGLE" });
expect(source.getSnapshot().matches("off")).toBe(true);
} finally {
source.stop();
}
});

This complete test imports redux.ts.

It checks a command result, external source updates, and unsubscribe behavior.

core.test.ts
import { expect, it } from "vitest";
import { core, source } from "./redux";
it("pairs native snapshots with states and observes external source changes", async () => {
const seen: number[] = [];
try {
const subscription = core.watch((next) => seen.push(next.count));
const result = await core.execute({ command: "increment" });
expect(result.snapshot.count).toBe(1);
expect(result.states.count).toBe(1);
expect(result.events).toEqual([]);
source.dispatch({ type: "counter/increment" });
expect(seen).toEqual([1, 2]);
subscription.unsubscribe();
source.dispatch({ type: "counter/increment" });
expect(seen).toEqual([1, 2]);
} finally {
core.dispose();
}
});

execute waits for its callback and observation window, not every asynchronous business operation.

Resolve fixture-owned ports and assert application-correlated outcomes for asynchronous work; keep cancellation and stale-result coverage.

For native events and derived notifications, also test repeated occurrences and one producer per name.

Save this beside the complete light-switch.tsx.

It checks actual accessible controls and source-derived state.

toggle.test.tsx
/** @jsxImportSource ignite-element/jsx */
import { expect, it } from "vitest";
import { within } from "@testing-library/dom";
import { core } from "./light-switch";
it("derives each light's label and flip count from its independent source", async () => {
const host = document.createElement("ignite-light-switch");
const sibling = document.createElement("ignite-light-switch");
try {
document.body.append(host, sibling);
const root = host.shadowRoot?.querySelector("section");
const siblingRoot = sibling.shadowRoot?.querySelector("section");
if (!root || !siblingRoot) throw new Error("Light switches did not render");
const controls = within(root);
const siblingControls = within(siblingRoot);
const firstSwitch = controls.getByRole("switch", { name: "Light" });
const secondSwitch = siblingControls.getByRole("switch", { name: "Light" });
expect(firstSwitch.getAttribute("aria-checked")).toBe("false");
expect(controls.getByText("Off")).toBeDefined();
expect(controls.getByText("Toggled: 0")).toBeDefined();
firstSwitch.click();
await Promise.resolve();
expect(firstSwitch.getAttribute("aria-checked")).toBe("true");
expect(controls.getByText("On")).toBeDefined();
expect(controls.getByText("Toggled: 1")).toBeDefined();
expect(secondSwitch.getAttribute("aria-checked")).toBe("false");
expect(siblingControls.getByText("Off")).toBeDefined();
expect(siblingControls.getByText("Toggled: 0")).toBeDefined();
firstSwitch.click();
await Promise.resolve();
expect(firstSwitch.getAttribute("aria-checked")).toBe("false");
expect(controls.getByText("Off")).toBeDefined();
expect(controls.getByText("Toggled: 2")).toBeDefined();
secondSwitch.click();
await Promise.resolve();
expect(secondSwitch.getAttribute("aria-checked")).toBe("true");
expect(siblingControls.getByText("On")).toBeDefined();
expect(siblingControls.getByText("Toggled: 1")).toBeDefined();
expect(controls.getByText("Toggled: 2")).toBeDefined();
} finally {
host.remove();
sibling.remove();
core.dispose();
}
});

Register each custom-element name once.

Use fresh fixture-owned sources or explicitly shared fixtures; do not dispose a core that later tests reuse.

Remove hosts, unsubscribe temporary observers, and terminally dispose test-owned cores, including registered cores.

Stop borrowed sources only when the test owns them.

Use try/finally around the entire acquisition/use boundary so failed assertions still clean up.

Test real disconnect/reconnect and same-tick moves when your behavior depends on them.

Do not infer lifecycle history or durable business receipts from a runtime result.

See ownership and the example index for broader fixtures.