Testing
Test behavior at its owner boundary: the source, the core, then the rendered controls.
Use ordinary assertions.
For a browser-like Vitest host:
pnpm add -D vitest jsdom @testing-library/domimport { defineConfig } from 'vitest/config';
export default defineConfig({ test: { environment: 'jsdom' },});Use the same JSX settings as the application.
Source behavior
Section titled “Source behavior”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(); }});Core contracts
Section titled “Core contracts”This complete test imports redux.ts.
It checks a command result, external source updates, and unsubscribe behavior.
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.
Rendered controls
Section titled “Rendered controls”Save this beside the complete light-switch.tsx.
It checks actual accessible controls and source-derived state.
/** @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(); }});Cleanup and lifetime
Section titled “Cleanup and lifetime”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.