Skip to main content

Quick Start

Scaffold Atomic Testing into your project with one command:

npm create atomic-testing@latest

Run it from inside an existing project — it scaffolds Atomic Testing into your app, it doesn't create the app. create atomic-testing:

  1. Detects your framework and major version, test runner, package manager, TypeScript, and design system from package.json, lockfiles, and config files.
  2. Prompts for anything it couldn't detect confidently — interactively in a terminal, or automatically (non-interactive) in CI.
  3. Resolves a recipe for your framework × runner × design system, and refuses impossible or unsupported combinations with a clear message.
  4. Writes a standalone runner config plus an example component, ScenePart, and a passing test under atomic-testing-example/, and adds a test script to package.json. By default it also writes four Claude Code skills under .claude/skills/ and a root CLAUDE.md guide adapted to your detected stack (skip both with --no-agents).
  5. Installs the dependencies after asking — or, with --no-install, prints the exact per-package-manager commands so you can run them yourself.

When it finishes you have a green example test in atomic-testing-example/, and the CLI prints the exact command to run it. Combinations outside the verified set (React + Jest, Vue 3 + Jest) are marked experimental — the CLI warns before writing one.

Three quick detours

What the CLI writes

For the canonical React + Jest stack, the scaffolder drops:

your-project/
├─ .claude/
│ └─ skills/
│ ├─ scaffold-test-driver/SKILL.md
│ ├─ author-component-tests/SKILL.md
│ ├─ diagnose-test-failure/SKILL.md
│ └─ sync-test-driver/SKILL.md
├─ CLAUDE.md # guide adapted to your detected framework, runner, and driver package
├─ jest.config.cjs # standalone runner config (.cjs sidesteps the ESM rename trap)
├─ package.json # gains a "test" script
└─ atomic-testing-example/
├─ ExampleComponent.tsx # a sample component to replace with your own
├─ scenePart.ts # maps a named part to a locator + driver
└─ ExampleComponent.test.tsx # a passing test

The skills and CLAUDE.md are on by default; pass --no-agents to skip them.

The generated test drives a real state change through the framework-agnostic HTML driver — HTMLTextInputDriver.setValue() then reads it back with getValue() — so on any in-process runner (Jest, Vitest, or Vitest browser mode) it passes on the first run, whatever framework you picked. Those stacks differ only in the details: Vue and Angular emit a .ts component, Vitest emits vitest.config.ts, and the example test filename follows the runner. Playwright is the exception — it drives a real browser against your running app, so the scaffolder emits playwright.config.ts plus a placeholder *.e2e starter test (no example component or scene part) that you point at your dev server; it turns green once your app serves the page, not out of the box.

Write it yourself

Prefer to author the test by hand — or just want to see what a richer one looks like? The example below is the same scene and test logic across React, Vue, and Playwright. It's the code the sections that follow unpack.

Before you run this

This test needs a jsdom-based runner (Jest or Vitest) with IS_REACT_ACT_ENVIRONMENT set. See Configure your test runner — one-time setup, applies to every example on this page. Installing the packages by hand? See Manual Installation.

import { useState } from 'react';

import { HTMLButtonDriver, HTMLElementDriver } from '@atomic-testing/component-driver-html';
import { byDataTestId, TestEngine } from '@atomic-testing/core';
import { createTestEngine } from '@atomic-testing/react-19';

// Component to test
function WelcomeButton({ name }: { name: string }) {
const [clicked, setClicked] = useState(false);

return (
<div>
<h1 data-testid='greeting'>Hello {name}!</h1>
<button data-testid='welcome-btn' onClick={() => setClicked(true)}>
{clicked ? 'Welcome!' : 'Click me'}
</button>
</div>
);
}

// Test setup
const welcomeScene = {
greeting: { locator: byDataTestId('greeting'), driver: HTMLElementDriver },
button: { locator: byDataTestId('welcome-btn'), driver: HTMLButtonDriver },
};

// Test
describe('WelcomeButton', () => {
let engine: TestEngine<typeof welcomeScene>;

beforeEach(() => {
engine = createTestEngine(<WelcomeButton name='Alice' />, welcomeScene);
});

afterEach(async () => {
await engine.cleanUp();
});

it('should welcome the user when clicked', async () => {
// Check initial state
expect(await engine.parts.greeting.getText()).toBe('Hello Alice!');
expect(await engine.parts.button.getText()).toBe('Click me');

// Interact and verify
await engine.parts.button.click();
expect(await engine.parts.button.getText()).toBe('Welcome!');
});
});

Run it:

npx jest

✨ The Magic

Notice how the test logic is identical across frameworks:

  1. Same Scene Definition: welcomeScene works everywhere
  2. Same Test Code: engine.parts.button.click() works everywhere
  3. Same Component Drivers: component-driver-html (or a component-library driver package, if you use one) works everywhere

This means: Learn once, test everywhere. Your testing knowledge transfers completely between frameworks.

🎯 What Just Happened?

  1. ScenePart: Defined which components matter (greeting, button)
  2. Locators: Found components using byDataTestId()
  3. Drivers: Used semantic APIs (click(), getText()) instead of DOM manipulation
  4. TestEngine: Orchestrated everything together

Next Steps

🏗️ Build Your First Real Test

Follow our step-by-step tutorial with a complete login form example.

Start Tutorial →

📦 Choose Your Packages

Not sure which packages you need? We'll help you decide based on your stack.

Package Guide →

🤔 Questions?

"This seems like extra setup..." → See Why Atomic Testing? for long-term benefits "How do I install without the CLI?" → See Manual Installation "How do I test complex forms?" → Check out the Step-by-Step Tutorial "What about my existing tests?" → We're working on a migration guide!