Skip to main content
v0.1.2 · MIT · Node ≥ 20

react-spec-gen

Generate Vitest + RTL tests and Storybook CSF 3 stories from your React components

# Try it instantly with npx
npx react-spec-gen src/components/Button.tsx

# Or install as a dev dependency
npm install -D react-spec-gen

Why react-spec-gen?

Built for teams that want consistent test and story coverage — without writing the same boilerplate twice.

Zero boilerplate

One command analyzes your component AST and emits a Vitest test plus a Storybook CSF 3 story right next to the source.

Context-aware inference

Extracts props from inline types, type aliases, interface, forwardRef and memo wrappers — then infers realistic prop values.

Offline & deterministic

No network calls by default. Reproducible output you can trust in CI and review on every run.

AI enhancement (preview)

Opt in with --ai to try the enhancer pipeline. Today it ships a deterministic mock provider — real LLM providers (Anthropic, Ollama) are on the roadmap.

Real testing patterns

Implicit ARIA role mapping, edge cases for booleans, optional props, union variants, and event-handler coverage out of the box.

Programmatic API

Use run() directly from Node to integrate with codegen pipelines, monorepo scripts, or custom tooling.

From component to test + story in one command

react-spec-gen statically analyzes your React component, infers realistic prop values, and writes a Vitest test file and a Storybook CSF 3 story right next to the source.

Your component

Button.tsx
// src/components/Button.tsx
type ButtonProps = {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick?: () => void;
};

export function Button({ label, variant = "primary", disabled, onClick }: ButtonProps) {
return (
<button disabled={disabled} onClick={onClick}>
{label}
</button>
);
}

Generated test

Button.test.tsx
// Button.test.tsx — generated by react-spec-gen
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { Button } from "./Button";

describe("Button", () => {
const baseProps = {
label: "Label",
variant: "primary" as const,
disabled: false,
onClick: vi.fn(),
};

it("renders with default props", () => {
render(<Button {...baseProps} />);
expect(screen.getByRole("button")).toBeDefined();
});

it("when disabled is true", () => {
const props = { ...baseProps, disabled: true };
render(<Button {...props} />);
});

it("when variant is secondary", () => {
const props = { ...baseProps, variant: "secondary" as const };
render(<Button {...props} />);
});
});

Generated story

Button.stories.tsx
// Button.stories.tsx — generated by react-spec-gen
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
title: "Components/Button",
component: Button,
};
export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
args: { label: "Label", variant: "primary" },
};

export const Secondary: Story = {
args: { label: "Label", variant: "secondary" },
};

export const Disabled: Story = {
args: { label: "Label", disabled: true },
};

Ready to remove test boilerplate?

Run it once on a component. Review the verification checklist. Commit.