# Component Test-Driven Development Loop
## Principle
Start every UI change with a failing component test (`cy.mount`, Playwright component test, or RTL `render`). Follow the Red-Green-Refactor cycle: write a failing test (red), make it pass with minimal code (green), then improve the implementation (refactor). Ship only after the cycle completes. Keep component tests under 100 lines, isolated with fresh providers per test, and validate accessibility alongside functionality.
## Rationale
Component TDD provides immediate feedback during development. Failing tests (red) clarify requirements before writing code. Minimal implementations (green) prevent over-engineering. Refactoring with passing tests ensures changes don't break functionality. Isolated tests with fresh providers prevent state bleed in parallel runs. Accessibility assertions catch usability issues early. Visual debugging (Cypress runner, Storybook, Playwright trace viewer) accelerates diagnosis when tests fail.
## Pattern Examples
### Example 1: Red-Green-Refactor Loop
**Context**: When building a new component, start with a failing test that describes the desired behavior. Implement just enough to pass, then refactor for quality.
**Implementation**:
```typescript
// Step 1: RED - Write failing test
// Button.cy.tsx (Cypress Component Test)
import { Button } from './Button';
describe('Button Component', () => {
it('should render with label', () => {
cy.mount();
cy.contains('Click Me').should('be.visible');
});
it('should call onClick when clicked', () => {
const onClickSpy = cy.stub().as('onClick');
cy.mount();
cy.get('button').click();
cy.get('@onClick').should('have.been.calledOnce');
});
});
// Run test: FAILS - Button component doesn't exist yet
// Error: "Cannot find module './Button'"
// Step 2: GREEN - Minimal implementation
// Button.tsx
type ButtonProps = {
label: string;
onClick?: () => void;
};
export const Button = ({ label, onClick }: ButtonProps) => {
return ;
};
// Run test: PASSES - Component renders and handles clicks
// Step 3: REFACTOR - Improve implementation
// Add disabled state, loading state, variants
type ButtonProps = {
label: string;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
};
export const Button = ({
label,
onClick,
disabled = false,
loading = false,
variant = 'primary'
}: ButtonProps) => {
return (
);
};
// Step 4: Expand tests for new features
describe('Button Component', () => {
it('should render with label', () => {
cy.mount();
cy.contains('Click Me').should('be.visible');
});
it('should call onClick when clicked', () => {
const onClickSpy = cy.stub().as('onClick');
cy.mount();
cy.get('button').click();
cy.get('@onClick').should('have.been.calledOnce');
});
it('should be disabled when disabled prop is true', () => {
cy.mount();
cy.get('button').should('be.disabled');
});
it('should show spinner when loading', () => {
cy.mount();
cy.get('[data-testid="spinner"]').should('be.visible');
cy.get('button').should('be.disabled');
});
it('should apply variant styles', () => {
cy.mount();
cy.get('button').should('have.class', 'btn-danger');
});
});
// Run tests: ALL PASS - Refactored component still works
// Playwright Component Test equivalent
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test.describe('Button Component', () => {
test('should call onClick when clicked', async ({ mount }) => {
let clicked = false;
const component = await mount(