ProvenPin Install

Preact components with Signals state, tested with Testing Library on jsdom: working versions (tested 2026-10-15)

A Preact 11 cart using @preact/signals signal and computed state plus a hooks-based async form, tested in Vitest on jsdom with @testing-library/preact and user-event, including re-render on signal change without new props.

passed4 passed · 0 failed2026-10-15 14:16 UTC
Status
passed
Last passed
2026-10-15 14:16 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions7
preact@11.0.1@preact/signals@2.11.3@testing-library/preact@3.2.4@testing-library/user-event@14.6.8jsdom@30.1.3vitest@5.0.4@types/node@26.6.5
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 38748988915

The code

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

package.json
{
  "name": "preact-signals-testing-library",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
  },
  "dependencies": {
    "preact": "11.0.1",
    "@preact/signals": "2.11.3"
  },
  "devDependencies": {
    "@testing-library/preact": "3.2.4",
    "@testing-library/user-event": "14.6.8",
    "jsdom": "30.1.3",
    "vitest": "5.0.4",
    "@types/node": "26.6.5"
  }
}
src/cart.tsx
/** @jsxImportSource preact */
import { signal, computed } from "@preact/signals";
import { useState } from "preact/hooks";

export type Item = { sku: string; name: string; priceCents: number; qty: number };

/** Store as module-level signals; createCart() gives tests a fresh instance. */
export function createCart() {
  const items = signal<Item[]>([]);
  const count = computed(() => items.value.reduce((n, i) => n + i.qty, 0));
  const totalCents = computed(() => items.value.reduce((s, i) => s + i.qty * i.priceCents, 0));
  return {
    items, count, totalCents,
    add(item: Omit<Item, "qty">) {
      const existing = items.value.find((i) => i.sku === item.sku);
      items.value = existing
        ? items.value.map((i) => (i.sku === item.sku ? { ...i, qty: i.qty + 1 } : i))
        : [...items.value, { ...item, qty: 1 }];
    },
    remove(sku: string) { items.value = items.value.filter((i) => i.sku !== sku); },
  };
}
export type Cart = ReturnType<typeof createCart>;

const money = (c: number) => `$${(c / 100).toFixed(2)}`;

export function CartBadge({ cart }: { cart: Cart }) {
  // Reading .value inside render subscribes this component to the signal.
  return <span aria-label="cart count">{cart.count.value}</span>;
}

export function CartView({ cart }: { cart: Cart }) {
  if (cart.items.value.length === 0) return <p>Your cart is empty</p>;
  return (
    <section>
      <ul>
        {cart.items.value.map((i) => (
          <li key={i.sku}>
            {i.name} x{i.qty} <button onClick={() => cart.remove(i.sku)} aria-label={`remove ${i.name}`}>Remove</button>
          </li>
        ))}
      </ul>
      <p>Total: <strong data-testid="total">{money(cart.totalCents.value)}</strong></p>
    </section>
  );
}

export function CouponForm({ onApply }: { onApply: (code: string) => Promise<boolean> }) {
  const [code, setCode] = useState("");
  const [status, setStatus] = useState<"idle" | "checking" | "ok" | "bad">("idle");
  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      setStatus("checking");
      setStatus((await onApply(code.trim().toUpperCase())) ? "ok" : "bad");
    }}>
      <label>Coupon <input value={code} onInput={(e) => setCode(e.currentTarget.value)} /></label>
      <button type="submit" disabled={!code.trim() || status === "checking"}>Apply</button>
      {status === "ok" && <p role="status">Coupon applied</p>}
      {status === "bad" && <p role="alert">Invalid coupon</p>}
    </form>
  );
}
test/cart.test.tsx
/** @jsxImportSource preact */
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/preact";
import userEvent from "@testing-library/user-event";
import { createCart, CartBadge, CartView, CouponForm } from "../src/cart.tsx";

afterEach(cleanup);

describe("signals store", () => {
  it("merges quantities and computes totals", () => {
    const cart = createCart();
    cart.add({ sku: "A", name: "Hat", priceCents: 1250 });
    cart.add({ sku: "A", name: "Hat", priceCents: 1250 });
    cart.add({ sku: "B", name: "Gloves", priceCents: 899 });
    expect(cart.count.value).toBe(3);
    expect(cart.totalCents.value).toBe(3399);
  });
});

describe("components", () => {
  it("badge re-renders when the signal changes, with no props change", async () => {
    const cart = createCart();
    render(<CartBadge cart={cart} />);
    expect(screen.getByLabelText("cart count").textContent).toBe("0");
    cart.add({ sku: "A", name: "Hat", priceCents: 1250 });
    expect(await screen.findByText("1")).toBeTruthy();
  });

  it("cart view lists items, totals and removes on click", async () => {
    const user = userEvent.setup();
    const cart = createCart();
    cart.add({ sku: "A", name: "Hat", priceCents: 1250 });
    cart.add({ sku: "B", name: "Gloves", priceCents: 899 });
    render(<CartView cart={cart} />);
    expect(screen.getByTestId("total").textContent).toBe("$21.49");
    await user.click(screen.getByRole("button", { name: "remove Hat" }));
    expect(screen.queryByText(/Hat/)).toBeNull();
    expect(screen.getByTestId("total").textContent).toBe("$8.99");
    await user.click(screen.getByRole("button", { name: "remove Gloves" }));
    expect(screen.getByText("Your cart is empty")).toBeTruthy();
  });

  it("coupon form normalises input, disables while checking, and shows the result", async () => {
    const user = userEvent.setup();
    let resolve!: (ok: boolean) => void;
    const onApply = vi.fn(() => new Promise<boolean>((r) => { resolve = r; }));
    render(<CouponForm onApply={onApply} />);
    const button = screen.getByRole("button", { name: "Apply" }) as HTMLButtonElement;
    expect(button.disabled).toBe(true);
    await user.type(screen.getByLabelText("Coupon"), "  save10 ");
    await user.click(button);
    expect(onApply).toHaveBeenCalledWith("SAVE10");
    expect(button.disabled).toBe(true);
    resolve(false);
    expect((await screen.findByRole("alert")).textContent).toBe("Invalid coupon");
  });
});
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "strict": true,
    "skipLibCheck": true
  }
}
vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: { environment: "jsdom" },
});

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.