/react-test
Enforce TDD workflow for React. Write React Testing Library tests first (behavior-focused, accessibility-first), then implement components. Detects Vitest or Jest and verifies coverage targets.
> /plugin marketplace add affaan-m/ECC > /plugin install ecc@ecc
How it fires
How this command gets triggered: by you, by Claude, or both.
- Fires itselfClaude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/react-test
Context preview
What this command does when you run it.
Enforce TDD workflow for React. Write React Testing Library tests first (behavior-focused, accessibility-first), then implement components. Detects Vitest or Jest and verifies coverage targets.
Command definition
react-test.mddescription: Enforce TDD workflow for React. Write React Testing Library tests first (behavior-focused, accessibility-first), then implement components. Detects Vitest or Jest and verifies coverage targets.
React TDD Command
This command enforces test-driven development for React using React Testing Library plus Vitest or Jest, detected at runtime.
What This Command Does
1. **Define Component Signature**: Scaffold the component, prop type, and exports 2. **Write Behavior Tests First**: RTL queries (role-first), `userEvent`, MSW for network — RED 3. **Run Tests**: Verify they fail for the right reason 4. **Implement Minimal Code**: Just enough to pass — GREEN 5. **Refactor**: Improve while keeping tests green 6. **Check Coverage**: Hit the targets in [rules/react/testing.md](../rules/react/testing.md)
When to Use
Use `/react-test` when:
- Implementing a new React component or custom hook
- Adding test coverage to an untested component
- Fixing a bug (write failing test first that reproduces it)
- Building forms, state machines, or accessibility-critical UI
- Onboarding to RTL + Vitest/Jest workflow
TDD Cycle
RED -> Write failing test for the next behavior
GREEN -> Implement minimal component code to pass
REFACTOR -> Improve component, tests stay green
REPEAT -> Next behavior
Runner Detection
test -f vitest.config.ts -o -f vitest.config.js -o -f vite.config.ts # Vitest
grep -l '"jest"' package.json # Jest
Prefer Vitest for new Vite-based projects; respect Jest for existing setups.
Example Session
User: /react-test I need a SearchInput component with debounced search
Agent:
# TDD Session: SearchInput
## Step 1: Define Signature
```tsx
// src/components/SearchInput.tsx
type Props = {
onSearch: (query: string) => void;
placeholder?: string;
debounceMs?: number;
};
export function SearchInput({ onSearch, placeholder, debounceMs }: Props) {
throw new Error("not implemented");
}Step 2: Write Behavior Tests (RED)
// src/components/SearchInput.test.tsx
import { describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SearchInput } from "./SearchInput";
describe("SearchInput", () => {
test("renders with placeholder", () => {
render(<SearchInput onSearch={() => {}} placeholder="Search users" />);
expect(screen.getByPlaceholderText("Search users")).toBeInTheDocument();
});
test("calls onSearch after typing", async () => {
vi.useFakeTimers();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole("textbox"), "alice");
expect(onSearch).not.toHaveBeenCalled(); // before debounce
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledWith("alice"); // after debounce
vi.useRealTimers();
});
test("does not call onSearch when typing pauses then continues", async () => {
vi.useFakeTimers();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole("textbox"), "ali");
vi.advanceTimersByTime(200); // mid-debounce
await user.type(screen.getByRole("textbox"), "ce");
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledTimes(1);
expect(onSearch).toHaveBeenCalledWith("alice");
vi.useRealTimers();
});
test("is keyboard reachable and accessible", () => {
render(<SearchInput onSearch={() => {}} />);
const input = screen.getByRole("textbox");
input.focus();
expect(input).toHaveFocus();
});
});Step 3: Run Tests — Verify FAIL
$ vitest run src/components/SearchInput.test.tsx
× src/components/SearchInput.test.tsx (4 tests) ✘ Error: not implemented
✓ Tests fail as expected.
Step 4: Implement Minimal Code (GREEN)
import { useEffect, useState } from "react";
export function SearchInput({ onSearch, placeholder, debounceMs = 300 }: Props) {
const [query, setQuery] = useState("");
useEffect(() => {
const id = setTimeout(() => onSearch(query), debounceMs);
return () => clearTimeout(id);
}, [query, onSearch, debounceMs]);
return (
<input
type="text"
value={query}
placeholder={placeholder}
onChange={(e) => setQuery(e.target.value)}
/>
);
}Step 5: Run Tests — Verify PASS
$ vitest run src/components/SearchInput.test.tsx
✓ src/components/SearchInput.test.tsx (4 tests) 47ms
Step 6: Coverage
$ vitest run --coverage src/components/SearchInput.test.tsx
% Stmts: 100 % Branch: 100 % Funcs: 100 % Lines: 100
TDD Complete!
## Test Patterns
### Behavior, not implementation
Use `getByRole`, `getByLabelText`, `getByText`. Avoid `container.querySelector` and asserting on component state.
### `userEvent.setup()` per test
```tsx
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /save/i }));MSW for network
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
server.use(http.post("/api/users", () => HttpResponse.json({ id: "1" }, { status: 201 })));Custom hooks
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);Accessibility
import { axe } from "vitest-axe";
expect(await axe(container)).toHaveNoViolations();Coverage Targets
| Layer | Target | |---|---| | Pure utilities | >=90% | | Custom hooks | >=85% | | Presentational components | >=80% | | Container componen
Read more
description: Enforce TDD workflow for React. Write React Testing Library tests first (behavior-focused, accessibility-first), then implement components. Detects Vitest or Jest and verifies coverage targets.
React TDD Command
This command enforces test-driven development for React using React Testing Library plus Vitest or Jest, detected at runtime.
What This Command Does
1. **Define Component Signature**: Scaffold the component, prop type, and exports 2. **Write Behavior Tests First**: RTL queries (role-first), `userEvent`, MSW for network — RED 3. **Run Tests**: Verify they fail for the right reason 4. **Implement Minimal Code**: Just enough to pass — GREEN 5. **Refactor**: Improve while keeping tests green 6. **Check Coverage**: Hit the targets in [rules/react/testing.md](../rules/react/testing.md)
When to Use
Use `/react-test` when:
- Implementing a new React component or custom hook
- Adding test coverage to an untested component
- Fixing a bug (write failing test first that reproduces it)
- Building forms, state machines, or accessibility-critical UI
- Onboarding to RTL + Vitest/Jest workflow
TDD Cycle
RED -> Write failing test for the next behavior GREEN -> Implement minimal component code to pass REFACTOR -> Improve component, tests stay green REPEAT -> Next behavior
Runner Detection
test -f vitest.config.ts -o -f vitest.config.js -o -f vite.config.ts # Vitest grep -l '"jest"' package.json # Jest
Prefer Vitest for new Vite-based projects; respect Jest for existing setups.
Example Session
User: /react-test I need a SearchInput component with debounced search
Agent:
# TDD Session: SearchInput
## Step 1: Define Signature
```tsx
// src/components/SearchInput.tsx
type Props = {
onSearch: (query: string) => void;
placeholder?: string;
debounceMs?: number;
};
export function SearchInput({ onSearch, placeholder, debounceMs }: Props) {
throw new Error("not implemented");
}Step 2: Write Behavior Tests (RED)
// src/components/SearchInput.test.tsx
import { describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SearchInput } from "./SearchInput";
describe("SearchInput", () => {
test("renders with placeholder", () => {
render(<SearchInput onSearch={() => {}} placeholder="Search users" />);
expect(screen.getByPlaceholderText("Search users")).toBeInTheDocument();
});
test("calls onSearch after typing", async () => {
vi.useFakeTimers();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole("textbox"), "alice");
expect(onSearch).not.toHaveBeenCalled(); // before debounce
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledWith("alice"); // after debounce
vi.useRealTimers();
});
test("does not call onSearch when typing pauses then continues", async () => {
vi.useFakeTimers();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole("textbox"), "ali");
vi.advanceTimersByTime(200); // mid-debounce
await user.type(screen.getByRole("textbox"), "ce");
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledTimes(1);
expect(onSearch).toHaveBeenCalledWith("alice");
vi.useRealTimers();
});
test("is keyboard reachable and accessible", () => {
render(<SearchInput onSearch={() => {}} />);
const input = screen.getByRole("textbox");
input.focus();
expect(input).toHaveFocus();
});
});Step 3: Run Tests — Verify FAIL
$ vitest run src/components/SearchInput.test.tsx × src/components/SearchInput.test.tsx (4 tests) ✘ Error: not implemented
✓ Tests fail as expected.
Step 4: Implement Minimal Code (GREEN)
import { useEffect, useState } from "react";
export function SearchInput({ onSearch, placeholder, debounceMs = 300 }: Props) {
const [query, setQuery] = useState("");
useEffect(() => {
const id = setTimeout(() => onSearch(query), debounceMs);
return () => clearTimeout(id);
}, [query, onSearch, debounceMs]);
return (
<input
type="text"
value={query}
placeholder={placeholder}
onChange={(e) => setQuery(e.target.value)}
/>
);
}Step 5: Run Tests — Verify PASS
$ vitest run src/components/SearchInput.test.tsx ✓ src/components/SearchInput.test.tsx (4 tests) 47ms
Step 6: Coverage
$ vitest run --coverage src/components/SearchInput.test.tsx % Stmts: 100 % Branch: 100 % Funcs: 100 % Lines: 100
TDD Complete!
## Test Patterns
### Behavior, not implementation
Use `getByRole`, `getByLabelText`, `getByText`. Avoid `container.querySelector` and asserting on component state.
### `userEvent.setup()` per test
```tsx
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /save/i }));MSW for network
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
server.use(http.post("/api/users", () => HttpResponse.json({ id: "1" }, { status: 201 })));Custom hooks
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);Accessibility
import { axe } from "vitest-axe";
expect(await axe(container)).toHaveNoViolations();Coverage Targets
| Layer | Target | |---|---| | Pure utilities | >=90% | | Custom hooks | >=85% | | Presentational components | >=80% | | Container componen
Your agent can write code, but ECC gives it a coordinated engineering system and toolbox: it plans before it builds, verifies changes with tests, reviews its own work from a fresh context, remembers what matters, and turns repeated wins into reusable skills
Repo: affaan-m/ECC
Other commands on ecc.
- /add-language-rules
Workflow command scaffold for add-language-rules in everything-claude-code.
Open command - /database-migration
Workflow command scaffold for database-migration in everything-claude-code.
Open command - /feature-development
Workflow command scaffold for feature-development in everything-claude-code.
Open command - /aside
Answer a quick side question without interrupting or losing context from the current task. Resume work automatically after answering.
Open command - /auto-update
Pull the latest ECC repo changes and reinstall the current managed targets.
Open command - /build-fix
Detect the project build system and incrementally fix build/type errors with minimal safe changes.
Open command

