Skip to content

Testing

@whisq/testing provides utilities for mounting components in a test environment, querying the DOM, and simulating user interactions. It works with any test runner — Vitest, Jest, or Node’s built-in test runner.

Install the testing package:

Terminal window
npm install -D @whisq/testing
import { describe, it, expect, afterEach } from "vitest";
import { render, cleanup, screen } from "@whisq/testing";
import { signal, component, div, button, span } from "@whisq/core";
afterEach(cleanup);
const Counter = component((props: { initial?: number }) => {
const count = signal(props.initial ?? 0);
return div(
button({ onclick: () => count.value-- }, "-"),
span(() => `${count.value}`),
button({ onclick: () => count.value++ }, "+"),
);
});
describe("Counter", () => {
it("renders initial value", () => {
render(Counter({ initial: 5 }));
expect(screen.getByText("5")).toBeTruthy();
});
});

render() mounts the component into the document. cleanup() removes it after each test. screen provides query methods to find elements.

screen.getByText("Submit"); // throws if not found
screen.queryByText("Submit"); // returns null if not found

Role queries use implicit ARIA roles — a <button> has role "button", an <input> has role "textbox":

screen.getByRole("button"); // finds <button>
screen.getByRole("textbox"); // finds <input>

Use fireEvent to simulate user interactions:

import { render, cleanup, screen, fireEvent } from "@whisq/testing";
it("increments on click", () => {
render(Counter({ initial: 0 }));
const plus = screen.getByText("+");
fireEvent.click(plus);
expect(screen.getByText("1")).toBeTruthy();
});
fireEvent.click(element);
fireEvent.input(element, { target: { value: "hello" } });
fireEvent.change(element, { target: { value: "new value" } });
fireEvent.submit(element);
fireEvent.keydown(element, { key: "Enter" });
fireEvent.keyup(element, { key: "Escape" });
fireEvent.focus(element);
fireEvent.blur(element);
import { signal, computed, component, form, input, button } from "@whisq/core";
import { render, cleanup, screen, fireEvent } from "@whisq/testing";
const LoginForm = component(() => {
const email = signal("");
const valid = computed(() => email.value.includes("@"));
return form(
input({
type: "email",
placeholder: "Email",
value: () => email.value,
oninput: (e) => email.value = e.target.value,
}),
button({ disabled: () => !valid.value }, "Sign In"),
);
});
describe("LoginForm", () => {
afterEach(cleanup);
it("disables submit when email is invalid", () => {
render(LoginForm({}));
const btn = screen.getByText("Sign In");
expect(btn.disabled).toBe(true);
});
it("enables submit when email is valid", () => {
render(LoginForm({}));
const input = screen.getByRole("textbox");
fireEvent.input(input, { target: { value: "user@example.com" } });
const btn = screen.getByText("Sign In");
expect(btn.disabled).toBe(false);
});
});
import { signal, component, div, p, when } from "@whisq/core";
const Alert = component((props: { show: boolean }) => {
const visible = signal(props.show);
return div(
when(() => visible.value, () => p("Alert is visible")),
);
});
it("shows alert when visible", () => {
render(Alert({ show: true }));
expect(screen.queryByText("Alert is visible")).toBeTruthy();
});
it("hides alert when not visible", () => {
render(Alert({ show: false }));
expect(screen.queryByText("Alert is visible")).toBeNull();
});

A recommended file structure for tests:

src/
components/
Counter.ts
Counter.test.ts # test file next to component
stores/
todos.ts
todos.test.ts # test store logic directly