AgentStack
SKILL verified MIT Self-run

Js Testing

skill-iwritec0de-app-dev-js-testing · by iwritec0de

>-

No reviews yet
0 installs
16 views
0.0% view→install

Install

$ agentstack add skill-iwritec0de-app-dev-js-testing

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Js Testing? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

JS Testing Skill — Vitest + React Testing Library for Next.js

Critical Rules

  1. Vitest is the test runner. Do not use Jest. All configuration, mocking, and assertions use the Vitest API (vi.fn(), vi.mock(), vi.spyOn(), etc.).
  2. React Testing Library (RTL) for component tests. Query elements the way a user would find them — by role, label, text, or placeholder. Never reach for getByTestId unless no semantic query is possible.
  3. Test user behavior, not implementation. Assert what the user sees and can interact with. Do not assert on component state, refs, internal variables, or hook return values directly (except when using renderHook).
  4. Never test React internals. Do not mock useState, useEffect, useRef, or any React built-in. If you need to control what a hook returns, extract the logic into a custom hook and test it with renderHook.
  5. Use MSW for API mocking. Mock at the network layer with Mock Service Worker, not by stubbing fetch or axios directly. This gives realistic request/response cycles and catches serialization issues.
  6. Prefer userEvent over fireEvent. userEvent simulates real browser behavior (focus, blur, keystrokes). fireEvent dispatches synthetic DOM events and should only be used for events userEvent does not support.
  7. Every test file must be self-contained. Shared setup belongs in vitest.setup.ts or a dedicated test utility file, not scattered across tests via implicit globals.

Component Testing

Basic Render Test

// components/greeting.tsx
export function Greeting({ name }: { name: string }) {
  return Hello, {name}!;
}
// components/greeting.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Greeting } from "./greeting";

describe("Greeting", () => {
  it("renders the user name", () => {
    render();

    expect(
      screen.getByRole("heading", { name: /hello, kelley/i })
    ).toBeInTheDocument();
  });
});

User Interaction Test (Click)

// components/counter.tsx
"use client";

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    
      Count: {count}
       setCount((c) => c + 1)}>Increment
    
  );
}
// components/counter.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { Counter } from "./counter";

describe("Counter", () => {
  it("increments the count when the button is clicked", async () => {
    const user = userEvent.setup();
    render();

    expect(screen.getByText("Count: 0")).toBeInTheDocument();

    await user.click(screen.getByRole("button", { name: /increment/i }));

    expect(screen.getByText("Count: 1")).toBeInTheDocument();
  });
});

User Interaction Test (Typing)

// components/search-input.tsx
"use client";

import { useState } from "react";

export function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
  const [query, setQuery] = useState("");

  return (
     {
        e.preventDefault();
        onSearch(query);
      }}
    >
      Search
       setQuery(e.target.value)}
      />
      Go
    
  );
}
// components/search-input.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { SearchInput } from "./search-input";

describe("SearchInput", () => {
  it("calls onSearch with the typed query on submit", async () => {
    const user = userEvent.setup();
    const handleSearch = vi.fn();

    render();

    await user.type(screen.getByLabelText(/search/i), "vitest");
    await user.click(screen.getByRole("button", { name: /go/i }));

    expect(handleSearch).toHaveBeenCalledWith("vitest");
    expect(handleSearch).toHaveBeenCalledTimes(1);
  });
});

Async Data Loading Test

// components/user-profile.tsx
"use client";

import { useEffect, useState } from "react";

interface User {
  id: number;
  name: string;
  email: string;
}

export function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return Loading profile...;
  if (!user) return User not found;

  return (
    
      {user.name}
      {user.email}
    
  );
}
// components/user-profile.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { http, HttpResponse } from "msw";
import { server } from "@/test/msw-server";
import { UserProfile } from "./user-profile";

describe("UserProfile", () => {
  it("shows loading state then renders user data", async () => {
    server.use(
      http.get("/api/users/1", () => {
        return HttpResponse.json({
          id: 1,
          name: "Kelley",
          email: "kelley@example.com",
        });
      })
    );

    render();

    expect(screen.getByText(/loading profile/i)).toBeInTheDocument();

    expect(
      await screen.findByRole("heading", { name: /kelley/i })
    ).toBeInTheDocument();
    expect(screen.getByText("kelley@example.com")).toBeInTheDocument();
  });
});

Form Submission Test

// components/contact-form.tsx
"use client";

import { useState } from "react";

export function ContactForm() {
  const [submitted, setSubmitted] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    const res = await fetch("/api/contact", {
      method: "POST",
      body: JSON.stringify({
        name: formData.get("name"),
        message: formData.get("message"),
      }),
      headers: { "Content-Type": "application/json" },
    });

    if (res.ok) setSubmitted(true);
  }

  if (submitted) return Thank you for your message!;

  return (
    
      Name
      

      Message
      

      Send
    
  );
}
// components/contact-form.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { http, HttpResponse } from "msw";
import { server } from "@/test/msw-server";
import { ContactForm } from "./contact-form";

describe("ContactForm", () => {
  it("submits the form and shows a success message", async () => {
    const user = userEvent.setup();

    server.use(
      http.post("/api/contact", () => {
        return HttpResponse.json({ success: true });
      })
    );

    render();

    await user.type(screen.getByLabelText(/name/i), "Kelley");
    await user.type(screen.getByLabelText(/message/i), "Hello there");
    await user.click(screen.getByRole("button", { name: /send/i }));

    expect(
      await screen.findByText(/thank you for your message/i)
    ).toBeInTheDocument();
  });
});

Error State Test

// components/contact-form.test.tsx (additional test)
describe("ContactForm", () => {
  // ... success test above ...

  it("shows an error message when submission fails", async () => {
    const user = userEvent.setup();

    server.use(
      http.post("/api/contact", () => {
        return HttpResponse.json(
          { error: "Server error" },
          { status: 500 }
        );
      })
    );

    render();

    await user.type(screen.getByLabelText(/name/i), "Kelley");
    await user.type(screen.getByLabelText(/message/i), "Hello");
    await user.click(screen.getByRole("button", { name: /send/i }));

    // The form should still be visible (not replaced by success message)
    expect(
      await screen.findByText(/something went wrong/i)
    ).toBeInTheDocument();
    expect(screen.getByRole("button", { name: /send/i })).toBeInTheDocument();
  });
});

API Route Testing (Next.js App Router)

Testing a GET Route Handler

// app/api/users/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const role = searchParams.get("role");

  const users = role
    ? await db.user.findMany({ where: { role } })
    : await db.user.findMany();

  return NextResponse.json(users);
}
// app/api/users/route.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { GET } from "./route";

vi.mock("@/lib/db", () => ({
  db: {
    user: {
      findMany: vi.fn(),
    },
  },
}));

import { db } from "@/lib/db";

const mockFindMany = vi.mocked(db.user.findMany);

describe("GET /api/users", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns all users when no role filter is provided", async () => {
    const users = [
      { id: 1, name: "Alice", role: "admin" },
      { id: 2, name: "Bob", role: "user" },
    ];
    mockFindMany.mockResolvedValue(users);

    const request = new Request("http://localhost/api/users");
    const response = await GET(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toEqual(users);
    expect(mockFindMany).toHaveBeenCalledWith();
  });

  it("filters users by role when query param is provided", async () => {
    const admins = [{ id: 1, name: "Alice", role: "admin" }];
    mockFindMany.mockResolvedValue(admins);

    const request = new Request("http://localhost/api/users?role=admin");
    const response = await GET(request);
    const data = await response.json();

    expect(data).toEqual(admins);
    expect(mockFindMany).toHaveBeenCalledWith({ where: { role: "admin" } });
  });
});

Testing a POST Route Handler

// app/api/posts/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "@/lib/auth";

export async function POST(request: Request) {
  const session = await getServerSession();

  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await request.json();

  if (!body.title || !body.content) {
    return NextResponse.json(
      { error: "Title and content are required" },
      { status: 400 }
    );
  }

  const post = await db.post.create({
    data: {
      title: body.title,
      content: body.content,
      authorId: session.user.id,
    },
  });

  return NextResponse.json(post, { status: 201 });
}
// app/api/posts/route.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { POST } from "./route";

vi.mock("@/lib/db", () => ({
  db: {
    post: {
      create: vi.fn(),
    },
  },
}));

vi.mock("@/lib/auth", () => ({
  getServerSession: vi.fn(),
}));

import { db } from "@/lib/db";
import { getServerSession } from "@/lib/auth";

const mockCreate = vi.mocked(db.post.create);
const mockGetSession = vi.mocked(getServerSession);

function jsonRequest(body: unknown): Request {
  return new Request("http://localhost/api/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
}

describe("POST /api/posts", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns 401 when the user is not authenticated", async () => {
    mockGetSession.mockResolvedValue(null);

    const response = await POST(jsonRequest({ title: "Hi", content: "Body" }));

    expect(response.status).toBe(401);
    expect(await response.json()).toEqual({ error: "Unauthorized" });
  });

  it("returns 400 when required fields are missing", async () => {
    mockGetSession.mockResolvedValue({ user: { id: "u1", name: "Alice" } });

    const response = await POST(jsonRequest({ title: "" }));

    expect(response.status).toBe(400);
  });

  it("creates a post and returns 201", async () => {
    mockGetSession.mockResolvedValue({ user: { id: "u1", name: "Alice" } });
    mockCreate.mockResolvedValue({
      id: "p1",
      title: "My Post",
      content: "Content here",
      authorId: "u1",
    } as any);

    const response = await POST(
      jsonRequest({ title: "My Post", content: "Content here" })
    );
    const data = await response.json();

    expect(response.status).toBe(201);
    expect(data.title).toBe("My Post");
    expect(mockCreate).toHaveBeenCalledWith({
      data: {
        title: "My Post",
        content: "Content here",
        authorId: "u1",
      },
    });
  });
});

Hook Testing

Testing a Custom Hook

// hooks/use-debounce.ts
import { useState, useEffect } from "react";

export function useDebounce(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}
// hooks/use-debounce.test.ts
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { useDebounce } from "./use-debounce";

describe("useDebounce", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("returns the initial value immediately", () => {
    const { result } = renderHook(() => useDebounce("hello", 500));
    expect(result.current).toBe("hello");
  });

  it("updates the value after the delay", () => {
    const { result, rerender } = renderHook(
      ({ value, delay }) => useDebounce(value, delay),
      { initialProps: { value: "hello", delay: 500 } }
    );

    rerender({ value: "world", delay: 500 });

    // Value should not have changed yet
    expect(result.current).toBe("hello");

    act(() => {
      vi.advanceTimersByTime(500);
    });

    expect(result.current).toBe("world");
  });
});

Testing an Async Hook with Providers

// hooks/use-current-user.ts
"use client";

import { useEffect, useState } from "react";

interface User {
  id: string;
  name: string;
}

export function useCurrentUser() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch("/api/me")
      .then((res) => {
        if (!res.ok) throw new Error("Failed to fetch user");
        return res.json();
      })
      .then(setUser)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  return { user, loading, error };
}
// hooks/use-current-user.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { http, HttpResponse } from "msw";
import { server } from "@/test/msw-server";
import { useCurrentUser } from "./use-current-user";

describe("useCurrentUser", () => {
  it("returns the current user after loading", async () => {
    server.use(
      http.get("/api/me", () => {
        return HttpResponse.json({ id: "u1", name: "Kelley" });
      })
    );

    const { result } = renderHook(() => useCurrentUser());

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.user).toEqual({ id: "u1", name: "Kelley" });
    expect(result.current.error).toBeNull();
  });

  it("returns an error when the API fails", async () => {
    server.use(
      http.get("/api/me", () => {
        return HttpResponse.json(null, { status: 500 });
      })
    );

    const { result } = renderHook(() => useCurrentUser());

    await waitFor(() => {
      expe

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [iwritec0de](https://github.com/iwritec0de)
- **Source:** [iwritec0de/app-dev](https://github.com/iwritec0de/app-dev)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.