ProvenPin Install

Vanilla DOM form testing with Testing Library + user-event on happy-dom (Vitest): working versions (tested 2026-10-15)

Test framework-free DOM code (a validated signup form) the way users use it, with @testing-library/dom queries and user-event typing on happy-dom.

passed3 passed · 0 failed2026-10-15 13:28 UTC
Status
passed
Last passed
2026-10-15 13:28 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions5
@types/node@24.19.2vitest@5.0.4@testing-library/dom@10.4.2@testing-library/user-event@14.6.8happy-dom@21.0.1
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("dom-testing-library-happy-dom") (install), or see them on GitHub.

package.json
{
  "name": "dom-testing-library-happy-dom",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
  },
  "dependencies": {},
  "devDependencies": {
    "@types/node": "24.19.2",
    "vitest": "5.0.4",
    "@testing-library/dom": "10.4.2",
    "@testing-library/user-event": "14.6.8",
    "happy-dom": "21.0.1"
  }
}
vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({ test: { environment: "happy-dom" } });
src/form.ts
export function mountSignup(root: HTMLElement, onSubmit: (data: { email: string }) => void) {
  root.innerHTML = `
    <form novalidate aria-label="Sign up">
      <label for="email">Email</label>
      <input id="email" name="email" type="email" aria-describedby="email-error" />
      <p id="email-error" role="alert" hidden></p>
      <button type="submit" disabled>Create account</button>
    </form>`;
  const form = root.querySelector("form")!;
  const input = root.querySelector<HTMLInputElement>("#email")!;
  const error = root.querySelector<HTMLParagraphElement>("#email-error")!;
  const button = root.querySelector("button")!;
  input.addEventListener("input", () => { button.disabled = input.value.trim() === ""; });
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    const ok = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.value);
    error.hidden = ok;
    error.textContent = ok ? "" : "Enter a valid email";
    input.setAttribute("aria-invalid", String(!ok));
    if (ok) onSubmit({ email: input.value.trim() });
  });
}
test/form.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import { mountSignup } from "../src/form";

let onSubmit: ReturnType<typeof vi.fn>;
beforeEach(() => { document.body.innerHTML = ""; onSubmit = vi.fn(); mountSignup(document.body, onSubmit); });

describe("signup form", () => {
  it("button enables once the user types", async () => {
    const user = userEvent.setup();
    const button = screen.getByRole("button", { name: "Create account" }) as HTMLButtonElement;
    expect(button.disabled).toBe(true);
    await user.type(screen.getByLabelText("Email"), "a");
    expect(button.disabled).toBe(false);
  });

  it("shows an accessible error for invalid email", async () => {
    const user = userEvent.setup();
    await user.type(screen.getByLabelText("Email"), "nope");
    await user.click(screen.getByRole("button"));
    expect(screen.getByRole("alert").textContent).toBe("Enter a valid email");
    expect(screen.getByLabelText("Email").getAttribute("aria-invalid")).toBe("true");
    expect(onSubmit).not.toHaveBeenCalled();
  });

  it("submits valid data with Enter", async () => {
    const user = userEvent.setup();
    await user.type(screen.getByLabelText("Email"), "[email protected]{Enter}");
    expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]" });
  });
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.

Related stacks

All @testing-library/dom recipesAll @testing-library/user-event recipesAll happy-dom recipesAll vitest recipes