Quick Start
Scaffold Atomic Testing into your project with one command:
- npm
- pnpm
- yarn
npm create atomic-testing@latest
pnpm create atomic-testing
yarn create atomic-testing
Run it from inside an existing project — it scaffolds Atomic Testing into your app, it doesn't create the app. create atomic-testing:
- Detects your framework and major version, test runner, package manager, TypeScript, and design system from
package.json, lockfiles, and config files. - Prompts for anything it couldn't detect confidently — interactively in a terminal, or automatically (non-interactive) in CI.
- Resolves a recipe for your framework × runner × design system, and refuses impossible or unsupported combinations with a clear message.
- Writes a standalone runner config plus an example component,
ScenePart, and a passing test underatomic-testing-example/, and adds atestscript topackage.json. By default it also writes four Claude Code skills under.claude/skills/and a rootCLAUDE.mdguide adapted to your detected stack (skip both with--no-agents). - 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.
- Prefer to wire it up by hand? → Manual Installation
- Is my stack supported? → Framework and Runner Support
- Just need the runner config? → Configure your test runner
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.
- ⚛️ React
- 💚 Vue 3
- 🎭 Playwright
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
Vue tests run under a jsdom-based runner (Jest or Vitest). Unlike the React setup, no IS_REACT_ACT_ENVIRONMENT flag is needed — VueInteractor calls Vue's own nextTick() internally. See Configure your test runner for the config, and Manual Installation for the packages.
import { HTMLButtonDriver, HTMLElementDriver } from '@atomic-testing/component-driver-html';
import { byDataTestId, TestEngine } from '@atomic-testing/core';
import { createTestEngine } from '@atomic-testing/vue-3';
import { ref } from 'vue';
// Component to test (SFC-like). createTestEngine() doesn't forward props into
// the rendered component, so — unlike React's <WelcomeButton name="Alice" /> —
// parametrize via a closure instead of a Vue `props` schema.
function makeWelcomeButton(name: string) {
return {
template: `
<div>
<h1 data-testid="greeting">Hello ${name}!</h1>
<button
data-testid="welcome-btn"
@click="handleClick"
>
{{ clicked ? 'Welcome!' : 'Click me' }}
</button>
</div>
`,
setup() {
const clicked = ref(false);
const handleClick = () => (clicked.value = true);
return { clicked, handleClick };
},
};
}
// Test setup (same shape as React!)
const welcomeScene = {
greeting: { locator: byDataTestId('greeting'), driver: HTMLElementDriver },
button: { locator: byDataTestId('welcome-btn'), driver: HTMLButtonDriver },
};
// Test (same logic as React!)
describe('WelcomeButton', () => {
let engine: TestEngine<typeof welcomeScene>;
beforeEach(() => {
engine = createTestEngine(makeWelcomeButton('Alice'), welcomeScene);
});
afterEach(async () => {
await engine.cleanUp();
});
it('should welcome the user when clicked', async () => {
// Same test logic as React version ✨
expect(await engine.parts.greeting.getText()).toBe('Hello Alice!');
await engine.parts.button.click();
expect(await engine.parts.button.getText()).toBe('Welcome!');
});
});
Run it:
npx vitest run
The React and Vue tabs render components directly into an in-memory jsdom document — nothing is served over HTTP. Playwright is different: page.goto('/welcome') drives a real browser against a page your app actually serves. The snippet below is illustrative — it only turns green once you have an app running that serves a /welcome route rendering the equivalent of the WelcomeButton component from the React tab (same data-testids). Point Playwright at that server with webServer/baseURL in playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'http://localhost:5173',
},
webServer: {
command: 'pnpm dev', // starts your app's dev server
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
Installing the packages by hand? See Manual Installation.
import { HTMLButtonDriver, HTMLElementDriver } from '@atomic-testing/component-driver-html';
import { byDataTestId, TestEngine } from '@atomic-testing/core';
import { createTestEngine } from '@atomic-testing/playwright';
import { test, expect } from '@playwright/test';
// Same scene definition as React/Vue ✨
const welcomeScene = {
greeting: { locator: byDataTestId('greeting'), driver: HTMLElementDriver },
button: { locator: byDataTestId('welcome-btn'), driver: HTMLButtonDriver },
};
// Assumes a served page at "/welcome" that renders a WelcomeButton with
// name="Alice" (see the caveat above) — this is illustrative, not copy-paste
// runnable against an empty project.
test.describe('WelcomeButton', () => {
let engine: TestEngine<typeof welcomeScene>;
test.beforeEach(async ({ page }) => {
// Navigate to your app (driven by baseURL from playwright.config.ts)
await page.goto('/welcome');
// Create test engine with same scene
engine = createTestEngine(page, welcomeScene);
});
test.afterEach(async () => {
await engine.cleanUp();
});
test('should welcome the user when clicked', async () => {
// Same test logic as React/Vue versions ✨
expect(await engine.parts.greeting.getText()).toBe('Hello Alice!');
await engine.parts.button.click();
expect(await engine.parts.button.getText()).toBe('Welcome!');
});
});
Once your app is served at the configured baseURL, run it:
npx playwright test
✨ The Magic
Notice how the test logic is identical across frameworks:
- Same Scene Definition:
welcomeSceneworks everywhere - Same Test Code:
engine.parts.button.click()works everywhere - 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?
- ScenePart: Defined which components matter (
greeting,button) - Locators: Found components using
byDataTestId() - Drivers: Used semantic APIs (
click(),getText()) instead of DOM manipulation - 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!