tdd-from-figma
Skill9 starsChanged 21 days agoMITView on GitHub
Writes failing tests FIRST from Figma structure and the design token lockfile, then implementation makes them pass. Per-component TDD cycle using exact values from design-tokens.lock.json. App-type-aware: generates Chrome extension, PWA, and web app test templates. Keywords: TDD, test-driven development, Figma tests, component testing, red-green-refactor, lockfile assertions, chrome extension tests, PWA tests
In this repository32 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- animation-motionSkill
- auth-flowsSkill
- build-from-canvaSkill
- build-from-conversationSkill
- build-from-figmaSkill
- build-from-screenshotSkill
- canva-intakeSkill
- canva-token-inferenceSkill
- ciSkill
- conversation-intakeSkill
- design-brief-to-figmaSkill
- design-token-lockSkill
- e2e-test-generatorSkill
- export-design-systemSkill
- figma-intakeSkill
- figma-to-react-workflowSkill
- form-handlingSkill
- indesign-conversionSkill
- lintSkill
- parallel-orchestrationSkill
- react-accessibilitySkill
- react-component-developmentSkill
- react-performance-optimizationSkill
- react-testing-workflowsSkill
- screenshot-intakeSkill
- seo-metadataSkill
- state-managementSkill
- storybook-story-generationSkill
- testSkill
- verify-allSkill
- visual-qa-verificationSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
18 rules. About 0 are common, 14 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈14
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 10
Write comprehensive test files for every component BEFORE writing implementation code.
- Distinctive: line 16
When adding new components to an existing Figma-derived codebase
- Distinctive: line 155
After writing all test files for a component batch, run:
- Distinctive: line 161
Expected: All tests FAIL (components don't exist yet).
- Distinctive: line 162
Test files have no syntax errors
- Distinctive: line 166
If any test passes unexpectedly, investigate — it may indicate a test that doesn't actually verify anything.
- Distinctive: line 173
Create component files at the paths tests import from
- Distinctive: line 174
Use design token classes from the lockfile
- Distinctive: line 178
Run after each component:
- Very specific: line 206
Every text string from the lockfile's textContent section
- Distinctive: line 207
Every interactive element has the correct ARIA role
- Distinctive: line 208
Every form input has an associated label
- Distinctive: line 209
Every image has alt text
- Distinctive: line 221
Write and verify tests for each batch before moving to the next.
- Very specific: line 225
Read build-spec.json field appType to determine which additional test templates to generate.
- Distinctive: line 229
When the app is a Chrome extension, generate these additional test categories:
- Distinctive: line 376
When the app is a PWA, generate these additional test categories:
- Very specific: line 622
Use @testing-library/react-native + Jest (Expo default — manifest.test.runner is jest):
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
TDD from Figma — Tests Before Components
Purpose
Write comprehensive test files for every component BEFORE writing implementation code. Tests use exact values from design-tokens.lock.json — text content, ARIA roles, component structure — so the implementation is constrained to match the design precisely. This eliminates label drift, missing accessibility attributes, and structural deviations.
When to Use
- Phase 3 of the
/build-from-figmapipeline (afterdesign-token-lock) - Any time you're about to generate React components from Figma and want TDD discipline
- When adding new components to an existing Figma-derived codebase
Inputs
- Required:
src/styles/design-tokens.lock.json(fromdesign-token-lockskill) - Required:
.claude/plans/build-spec.json(fromfigma-intakeskill) - Optional: Figma MCP access for additional structural context
TDD Cycle (Per Component)
Red Phase: Write Failing Tests
For each component in build-spec.json, write a test file BEFORE any implementation exists.
Test file location: Mirror the component path with .test.tsx suffix:
src/components/ui/Button.tsx→src/components/ui/Button.test.tsxsrc/components/sections/HeroSection.tsx→src/components/sections/HeroSection.test.tsx
Test categories to cover:
1. Rendering Tests
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
it("renders without crashing", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button")).toBeInTheDocument();
});
});2. Text Content Tests (from lockfile)
// Values pulled directly from design-tokens.lock.json textContent
it("displays correct heading text", () => {
render(<HeroSection />);
expect(screen.getByText("Build faster with AI")).toBeInTheDocument();
});
it("displays correct subheading", () => {
render(<HeroSection />);
expect(screen.getByText("Ship production apps in days, not months")).toBeInTheDocument();
});
it("displays correct CTA text", () => {
render(<HeroSection />);
expect(screen.getByRole("button", { name: "Get Started" })).toBeInTheDocument();
});3. Accessibility Tests
it("has correct heading hierarchy", () => {
render(<HeroSection />);
const h1 = screen.getByRole("heading", { level: 1 });
expect(h1).toHaveTextContent("Build faster with AI");
});
it("buttons are keyboard accessible", () => {
render(<Button>Submit</Button>);
const button = screen.getByRole("button");
expect(button).not.toHaveAttribute("tabindex", "-1");
});
it("form inputs have labels", () => {
render(<SearchInput />);
expect(screen.getByLabelText("Main keyword")).toBeInTheDocument();
});
it("images have alt text", () => {
render(<HeroSection />);
const images = screen.getAllByRole("img");
images.forEach((img) => {
expect(img).toHaveAttribute("alt");
expect(img.getAttribute("alt")).not.toBe("");
});
});4. Component Structure Tests
it("renders navigation links", () => {
render(<Header />);
expect(screen.getByRole("navigation")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Home" })).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Pricing" })).toBeInTheDocument();
});
it("renders all feature cards", () => {
render(<FeaturesGrid />);
const cards = screen.getAllByRole("article");
expect(cards).toHaveLength(3); // Match Figma frame count
});5. Variant Tests
it("renders primary variant", () => {
const { container } = render(<Button variant="primary">Click</Button>);
expect(container.firstChild).toHaveClass("bg-primary");
});
it("renders secondary variant", () => {
const { container } = render(<Button variant="secondary">Click</Button>);
expect(container.firstChild).toHaveClass("bg-secondary");
});
it("renders disabled state", () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});6. Interaction Tests
import userEvent from "@testing-library/user-event";
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledOnce();
});
it("toggles mobile menu on hamburger click", async () => {
const user = userEvent.setup();
render(<Header />);
const menuButton = screen.getByRole("button", { name: /menu/i });
await user.click(menuButton);
expect(screen.getByRole("navigation")).toBeVisible();
});Confirm Red
After writing all test files for a component batch, run:
pnpm vitest run --reporter=verboseExpected: All tests FAIL (components don't exist yet). This confirms:
- Test files have no syntax errors
- Imports reference the correct paths (that will be created)
- Assertions are meaningful (not vacuously true)
If any test passes unexpectedly, investigate — it may indicate a test that doesn't actually verify anything.
A --vite or --react scaffold ships a starter test for create-vite's App, src/App.test.tsx. It passes before you write anything, so delete it when the build replaces App.
Green Phase: Implementation
Hand off to the figma-react-converter agent or figma-to-react-workflow skill. The implementation must:
- Create component files at the paths tests import from
- Use design token classes from the lockfile
- Include exact text content from the lockfile
- Pass all written tests
Run after each component:
pnpm vitest run [component-path] --reporter=verboseRefactor Phase
Once green, refactor while keeping tests passing:
- Extract shared patterns into utility components
- Consolidate duplicate Tailwind class patterns
- Optimize component composition
Test Generation Rules
Query Priority (per React Testing Library best practices)
getByRole— always preferredgetByLabelText— for form elementsgetByPlaceholderText— fallback for inputsgetByText— for non-interactive textgetByTestId— last resort only
What NOT to Test
- Tailwind class names directly (brittle, test visual outcome instead)
- Internal component state (test behavior, not implementation)
- Third-party library internals
- Exact pixel values in tests (that's what visual QA is for)
What to ALWAYS Test
- Every text string from the lockfile's
textContentsection - Every interactive element has the correct ARIA role
- Every form input has an associated label
- Every image has alt text
- Component renders without errors
- Key user interactions trigger expected behavior
Batch Strategy
Process components in dependency order:
- UI primitives (Button, Input, Card, Badge) — no dependencies
- Layout (Header, Footer, Sidebar) — may use primitives
- Sections (HeroSection, FeaturesGrid) — use primitives + layout
- Pages (HomePage, PricingPage) — compose everything
Write and verify tests for each batch before moving to the next.
App-Type-Aware Test Generation
Read build-spec.json field appType to determine which additional test templates to generate.
Chrome Extension Tests (appType: "chrome-extension")
When the app is a Chrome extension, generate these additional test categories:
Manifest Validation Tests
import { readFileSync } from "fs";
import { describe, it, expect } from "vitest";
describe("Chrome Extension Manifest", () => {
const manifest = JSON.parse(readFileSync("public/manifest.json", "utf-8"));
it("has required manifest fields", () => {
expect(manifest.manifest_version).toBe(3);
expect(manifest.name).toBeTruthy();
expect(manifest.version).toBeTruthy();
});
it("declares required permissions", () => {
// Adapt to build-spec.json e2e.extensionManifest.permissions
expect(manifest.permissions).toContain("storage");
});
it("has popup defined", () => {
expect(manifest.action?.default_popup).toBeTruthy();
});
it("has service worker defined", () => {
expect(manifest.background?.service_worker).toBeTruthy();
});
it("has content scripts for correct URL patterns", () => {
// Adapt matches from build-spec.json e2e.extensionManifest.contentScriptMatches
const matches = manifest.content_scripts?.[0]?.matches;
expect(matches).toBeDefined();
expect(matches.length).toBeGreaterThan(0);
});
});Popup Component Tests
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock Chrome APIs for Vitest (jsdom doesn't have them)
const chromeMock = {
storage: {
local: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
},
sync: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
},
},
runtime: {
sendMessage: vi.fn(),
onMessage: { addListener: vi.fn() },
lastError: null,
},
tabs: {
query: vi.fn().mockResolvedValue([]),
sendMessage: vi.fn(),
},
};
beforeEach(() => {
vi.stubGlobal("chrome", chromeMock);
vi.clearAllMocks();
});
// Then write standard component tests for popup components
// using the same patterns as web app components.Background Service Worker Tests
import { describe, it, expect, vi, beforeEach } from "vitest";
// Test message handling
describe("Background Service Worker", () => {
it("responds to known message types", async () => {
// Import the handler function (not the full service worker registration)
const { handleMessage } = await import("../background/handler");
const response = await handleMessage({ type: "PING" });
expect(response).toBeDefined();
});
it("stores data to chrome.storage on relevant events", async () => {
// Test storage interactions via the handler
});
});Chrome API Mock Setup
For Chrome extension TDD, generate a shared mock file:
File: src/test/chrome-mock.ts
import { vi } from "vitest";
export const createChromeMock = () => ({
storage: {
local: {
get: vi.fn().mockImplementation((keys) => Promise.resolve({})),
set: vi.fn().mockImplementation(() => Promise.resolve()),
remove: vi.fn().mockImplementation(() => Promise.resolve()),
clear: vi.fn().mockImplementation(() => Promise.resolve()),
},
sync: {
get: vi.fn().mockImplementation((keys) => Promise.resolve({})),
set: vi.fn().mockImplementation(() => Promise.resolve()),
},
onChanged: { addListener: vi.fn(), removeListener: vi.fn() },
},
runtime: {
sendMessage: vi.fn(),
onMessage: { addListener: vi.fn(), removeListener: vi.fn() },
onInstalled: { addListener: vi.fn() },
getURL: vi.fn((path) => `chrome-extension://test-id/${path}`),
lastError: null,
},
tabs: {
query: vi.fn().mockResolvedValue([{ id: 1, url: "https://example.com" }]),
sendMessage: vi.fn(),
onUpdated: { addListener: vi.fn() },
onActivated: { addListener: vi.fn() },
},
scripting: {
executeScript: vi.fn().mockResolvedValue([{ result: null }]),
},
action: {
setBadgeText: vi.fn(),
setBadgeBackgroundColor: vi.fn(),
},
});
export const installChromeMock = () => {
const mock = createChromeMock();
vi.stubGlobal("chrome", mock);
return mock;
};PWA Tests (appType: "pwa")
When the app is a PWA, generate these additional test categories:
Web App Manifest Tests
import { readFileSync } from "fs";
import { describe, it, expect } from "vitest";
describe("PWA Manifest", () => {
const manifest = JSON.parse(readFileSync("public/manifest.json", "utf-8"));
it("has required PWA fields", () => {
expect(manifest.name).toBeTruthy();
expect(manifest.short_name).toBeTruthy();
expect(manifest.start_url).toBeTruthy();
expect(manifest.display).toBe("standalone");
});
it("has icons at required sizes", () => {
const sizes = manifest.icons.map((i: { sizes: string }) => i.sizes);
expect(sizes).toContain("192x192");
expect(sizes).toContain("512x512");
});
it("has theme and background colors", () => {
expect(manifest.theme_color).toBeTruthy();
expect(manifest.background_color).toBeTruthy();
});
});Service Worker Registration Tests
describe("Service Worker", () => {
it("registers on page load", () => {
// Test that navigator.serviceWorker.register is called
});
it("caches critical assets on install", () => {
// Test the install event handler
});
it("serves cached content when offline", () => {
// Test the fetch event handler with network failure
});
});Conditional Test Generation Logic
1. Read build-spec.json → appType
2. IF appType === "chrome-extension":
a. Generate chrome-mock.ts in src/test/
b. Generate manifest.test.ts
c. For each popup component: generate tests with Chrome API mocks
d. If build-spec has background script: generate background handler tests
e. If build-spec has content scripts: generate content script tests
3. IF appType === "pwa":
a. Generate manifest.test.ts (PWA version)
b. Generate service worker registration tests
c. Generate offline fallback tests
4. ALWAYS: Generate standard component tests (existing behavior)Renderer-Aware Test Generation
Resolve the build-spec's renderer manifest to determine the test runner and library:
node scripts/renderer-registry.js resolve <renderer> --jsonRead manifest.test:
runner—vitestorjestlibrary— e.g.@testing-library/react,@vue/test-utils,@testing-library/svelte,@testing-library/react-nativesetup(optional) — test setup file to wire upcontainerApi(optional) — whentrue, the framework supports server/static container rendering (used by Astro static.astrocomponents; render via the framework's Container API rather than a DOM testing library)
The component patterns below are grouped by manifest.language. Default is React (@testing-library/react + Vitest, the existing behavior above).
Vue 3 Tests (language: "vue", library: @vue/test-utils)
Use @vue/test-utils + Vitest:
Rendering Tests
import { mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import Button from "./Button.vue";
describe("Button", () => {
it("renders without crashing", () => {
const wrapper = mount(Button, { slots: { default: "Click me" } });
expect(wrapper.find("button").exists()).toBe(true);
});
});Text Content Tests (from lockfile)
it("displays correct heading text", () => {
const wrapper = mount(HeroSection);
expect(wrapper.text()).toContain("Build faster with AI");
});
it("displays correct CTA text", () => {
const wrapper = mount(HeroSection);
expect(wrapper.find("button").text()).toBe("Get Started");
});Accessibility Tests
it("has correct heading hierarchy", () => {
const wrapper = mount(HeroSection);
const h1 = wrapper.find("h1");
expect(h1.exists()).toBe(true);
expect(h1.text()).toBe("Build faster with AI");
});
it("form inputs have labels", () => {
const wrapper = mount(SearchInput);
expect(wrapper.find("label").exists()).toBe(true);
});Props and Emits Tests
it("emits click event", async () => {
const wrapper = mount(Button, { slots: { default: "Click" } });
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("click")).toHaveLength(1);
});
it("renders primary variant", () => {
const wrapper = mount(Button, {
props: { variant: "primary" },
slots: { default: "Click" },
});
expect(wrapper.find("button").classes()).toContain("bg-primary");
});
it("renders disabled state", () => {
const wrapper = mount(Button, {
props: { disabled: true },
slots: { default: "Click" },
});
expect(wrapper.find("button").attributes("disabled")).toBeDefined();
});Slot Tests
it("renders default slot content", () => {
const wrapper = mount(Card, {
slots: { default: "<p>Card content</p>" },
});
expect(wrapper.find("p").text()).toBe("Card content");
});
it("renders named slots", () => {
const wrapper = mount(Card, {
slots: {
header: "<h3>Title</h3>",
default: "<p>Body</p>",
footer: "<button>Action</button>",
},
});
expect(wrapper.find("h3").text()).toBe("Title");
expect(wrapper.find("p").text()).toBe("Body");
expect(wrapper.find("button").text()).toBe("Action");
});Svelte Tests (language: "svelte", library: @testing-library/svelte)
Use @testing-library/svelte + Vitest:
Rendering Tests
import { render, screen } from "@testing-library/svelte";
import { describe, it, expect } from "vitest";
import Button from "./Button.svelte";
describe("Button", () => {
it("renders without crashing", () => {
render(Button, { props: { children: "Click me" } });
expect(screen.getByRole("button")).toBeInTheDocument();
});
});Text Content Tests (from lockfile)
it("displays correct heading text", () => {
render(HeroSection);
expect(screen.getByText("Build faster with AI")).toBeInTheDocument();
});
it("displays correct CTA text", () => {
render(HeroSection);
expect(screen.getByRole("button", { name: "Get Started" })).toBeInTheDocument();
});Event Tests
import { fireEvent } from "@testing-library/svelte";
it("fires click event", async () => {
const onclick = vi.fn();
render(Button, { props: { onclick } });
await fireEvent.click(screen.getByRole("button"));
expect(onclick).toHaveBeenCalledOnce();
});Props Tests
it("renders primary variant", () => {
const { container } = render(Button, { props: { variant: "primary" } });
expect(container.querySelector("button")).toHaveClass("bg-primary");
});
it("renders disabled state", () => {
render(Button, { props: { disabled: true } });
expect(screen.getByRole("button")).toBeDisabled();
});Accessibility Tests
it("has correct heading hierarchy", () => {
render(HeroSection);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Build faster with AI");
});
it("navigation has correct landmark", () => {
render(Header);
expect(screen.getByRole("navigation")).toBeInTheDocument();
});React Native Tests (language: "react-native", runner: jest, library: @testing-library/react-native)
Use @testing-library/react-native + Jest (Expo default — manifest.test.runner is jest):
Rendering Tests
import { render, screen } from "@testing-library/react-native";
import { describe, it, expect } from "@jest/globals";
import { Button } from "./Button";
describe("Button", () => {
it("renders without crashing", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button")).toBeTruthy();
});
});Text Content Tests (from lockfile)
it("displays correct heading text", () => {
render(<HeroSection />);
expect(screen.getByText("Build faster with AI")).toBeTruthy();
});Accessibility Tests
it("button has accessibility role", () => {
render(<Button>Submit</Button>);
expect(screen.getByRole("button")).toBeTruthy();
});
it("icon button has accessibility label", () => {
render(<IconButton icon="search" accessibilityLabel="Search" />);
expect(screen.getByLabelText("Search")).toBeTruthy();
});
it("images have accessibility labels", () => {
render(<HeroSection />);
const images = screen.getAllByRole("image");
images.forEach((img) => {
expect(img.props.accessibilityLabel || img.props.accessible).toBeTruthy();
});
});Interaction Tests
import { fireEvent } from "@testing-library/react-native";
it("calls onPress when pressed", () => {
const onPress = jest.fn();
render(<Button onPress={onPress}>Click</Button>);
fireEvent.press(screen.getByRole("button"));
expect(onPress).toHaveBeenCalledOnce();
});
it("does not fire onPress when disabled", () => {
const onPress = jest.fn();
render(<Button onPress={onPress} disabled>Click</Button>);
fireEvent.press(screen.getByRole("button"));
expect(onPress).not.toHaveBeenCalled();
});Variant Tests
it("renders primary variant styling", () => {
const { toJSON } = render(<Button variant="primary">Click</Button>);
// NativeWind applies classes at render
expect(toJSON()).toBeTruthy();
});Conditional Test Generation Logic (Updated)
1. Read build-spec.json → renderer, appType
2. Resolve the renderer manifest: renderer-registry.js resolve <renderer> --json
3. Select the test runner + library directly from manifest.test:
- runner: manifest.test.runner (vitest | jest)
- library: manifest.test.library (@testing-library/react | @vue/test-utils |
@testing-library/svelte | @testing-library/react-native | ...)
- setup: manifest.test.setup (wire up if present)
- containerApi: manifest.test.containerApi (static container rendering, see Astro note)
4. Generate component tests using the patterns for manifest.language
5. THEN apply app-type-specific tests (existing chrome-extension/pwa logic)
- Note: chrome-extension and pwa app types only apply to web targets (react/vue/svelte)
- react-native does not use chrome-extension or pwa test templatesAstro Test Split
For renderer: "astro", manifest.language is react and manifest.test.containerApi
is true. Split tests by what is under test:
- Islands (interactive React/Vue/Svelte components) → the island's framework
runner + library from
manifest.test(e.g. Vitest +@testing-library/react). - Static
.astrocomponents → Vitest + the Astro Container API (manifest.test.containerApi === true): render the component to a string/fragment and assert on the output rather than using a DOM testing library. - Page-level interactivity (hydration, cross-island behavior) → Playwright E2E, generated in Phase 6 (e2e-test-generator), not here.
Output
| File | Purpose |
src/components/**/*.test.tsx | Test files for every component |
| Console output | Red/Green status per test run |
Integration
- Reads from:
design-tokens.lock.json(exact values),build-spec.json(component inventory) - Feeds into:
figma-to-react-workflowPhase 2 (implementation must pass these tests) - References:
react-testing-workflowsskill for testing patterns and Vitest configuration - Verified by:
pnpm vitest run --coveragein the quality gate
Version: 2.0.0
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

