CODITECT Pattern Library: Test-Driven Development Specification
Version: 1.0
Purpose: Automated testing framework for design patterns
Testing Philosophy
Test-Driven Design: Every pattern has:
- Visual Tests - Appearance matches specification
- Functional Tests - Interactions work correctly
- Accessibility Tests - WCAG 2.1 AA compliance
- Integration Tests - Patterns compose properly
Test Suite Structure
tests/
├── unit/
│ ├── atoms/
│ │ ├── button.test.js
│ │ ├── input.test.js
│ │ └── ...
│ ├── molecules/
│ └── organisms/
├── integration/
│ ├── dashboard.test.js
│ ├── detail-view.test.js
│ └── ...
├── visual/
│ ├── snapshots/
│ └── regression.test.js
└── accessibility/
└── a11y.test.js
ATOM TESTS
Button Test Suite
import { test, expect } from '@playwright/test';
import { checkA11y, injectAxe } from 'axe-playwright';
describe('Button Atom', () => {
describe('Visual Requirements', () => {
test('primary button has correct styles', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
// Height
await expect(btn).toHaveCSS('height', '40px');
// Border radius
await expect(btn).toHaveCSS('border-radius', '6px');
// Font weight
await expect(btn).toHaveCSS('font-weight', '500');
// Background color
await expect(btn).toHaveCSS('background-color', 'rgb(59, 130, 246)');
});
test('button has minimum touch target size', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
const box = await btn.boundingBox();
expect(box.height).toBeGreaterThanOrEqual(44);
expect(box.width).toBeGreaterThanOrEqual(44);
});
test('visual regression: button states', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
// Default state
await expect(btn).toHaveScreenshot('button-default.png');
// Hover state
await btn.hover();
await expect(btn).toHaveScreenshot('button-hover.png');
// Focus state
await btn.focus();
await expect(btn).toHaveScreenshot('button-focus.png');
// Disabled state
await page.goto('/components/atoms/button?state=disabled');
const btnDisabled = page.locator('[data-testid="btn-disabled"]');
await expect(btnDisabled).toHaveScreenshot('button-disabled.png');
});
});
describe('Functional Requirements', () => {
test('button is clickable', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
let clicked = false;
await page.exposeFunction('handleClick', () => { clicked = true; });
await page.evaluate(() => {
document.querySelector('[data-testid="btn-primary"]')
.addEventListener('click', () => window.handleClick());
});
await btn.click();
expect(clicked).toBe(true);
});
test('disabled button is not clickable', async ({ page }) => {
await page.goto('/components/atoms/button?state=disabled');
const btn = page.locator('[data-testid="btn-disabled"]');
let clicked = false;
await page.exposeFunction('handleClick', () => { clicked = true; });
await page.evaluate(() => {
document.querySelector('[data-testid="btn-disabled"]')
.addEventListener('click', () => window.handleClick());
});
await btn.click({ force: true });
expect(clicked).toBe(false);
});
test('button shows loading state', async ({ page }) => {
await page.goto('/components/atoms/button?state=loading');
const btn = page.locator('[data-testid="btn-loading"]');
// Spinner present
const spinner = btn.locator('.spinner');
await expect(spinner).toBeVisible();
// Button disabled
await expect(btn).toBeDisabled();
});
});
describe('Accessibility Requirements', () => {
test('button is keyboard accessible', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
// Focus with Tab
await page.keyboard.press('Tab');
await expect(btn).toBeFocused();
// Activate with Enter
let activated = false;
await page.exposeFunction('handleActivate', () => { activated = true; });
await page.evaluate(() => {
document.querySelector('[data-testid="btn-primary"]')
.addEventListener('click', () => window.handleActivate());
});
await page.keyboard.press('Enter');
expect(activated).toBe(true);
});
test('button has visible focus indicator', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
await btn.focus();
// Check for outline or box-shadow
const outline = await btn.evaluate(el =>
getComputedStyle(el).outline
);
const boxShadow = await btn.evaluate(el =>
getComputedStyle(el).boxShadow
);
expect(outline !== 'none' || boxShadow !== 'none').toBe(true);
});
test('icon button has aria-label', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-icon"]');
const ariaLabel = await btn.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
expect(ariaLabel.length).toBeGreaterThan(0);
});
test('button passes accessibility audit', async ({ page }) => {
await page.goto('/components/atoms/button');
await injectAxe(page);
const results = await checkA11y(page, '[data-testid="btn-primary"]', {
detailedReport: true
});
expect(results.violations).toHaveLength(0);
});
test('button meets color contrast requirements', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
const bgColor = await btn.evaluate(el =>
getComputedStyle(el).backgroundColor
);
const color = await btn.evaluate(el =>
getComputedStyle(el).color
);
const contrast = calculateContrast(bgColor, color);
expect(contrast).toBeGreaterThanOrEqual(4.5);
});
});
describe('Responsive Behavior', () => {
test('button adapts to viewport size', async ({ page }) => {
await page.goto('/components/atoms/button');
const btn = page.locator('[data-testid="btn-primary"]');
// Desktop
await page.setViewportSize({ width: 1440, height: 900 });
const desktopWidth = await btn.evaluate(el => el.offsetWidth);
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
const mobileWidth = await btn.evaluate(el => el.offsetWidth);
// Button doesn't break layout
expect(mobileWidth).toBeLessThanOrEqual(375);
expect(mobileWidth).toBeGreaterThan(0);
});
});
});
// Helper function
function calculateContrast(rgb1, rgb2) {
const l1 = relativeLuminance(rgb1);
const l2 = relativeLuminance(rgb2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function relativeLuminance(rgb) {
const [r, g, b] = rgb.match(/\d+/g).map(n => {
const val = n / 255;
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
MOLECULE TESTS
Status Indicator Test Suite
describe('Status Indicator Molecule', () => {
describe('Composition Tests', () => {
test('contains required atoms', async ({ page }) => {
await page.goto('/components/molecules/status-indicator');
const indicator = page.locator('[data-testid="status-indicator"]');
// Contains status dot
const dot = indicator.locator('.status-dot');
await expect(dot).toBeVisible();
// Contains label
const label = indicator.locator('.status-label');
await expect(label).toBeVisible();
});
test('atoms are properly aligned', async ({ page }) => {
await page.goto('/components/molecules/status-indicator');
const indicator = page.locator('[data-testid="status-indicator"]');
const display = await indicator.evaluate(el =>
getComputedStyle(el).display
);
expect(display).toBe('inline-flex');
const alignItems = await indicator.evaluate(el =>
getComputedStyle(el).alignItems
);
expect(alignItems).toBe('center');
});
test('has correct spacing between atoms', async ({ page }) => {
await page.goto('/components/molecules/status-indicator');
const indicator = page.locator('[data-testid="status-indicator"]');
const gap = await indicator.evaluate(el =>
getComputedStyle(el).gap
);
expect(gap).toBe('8px');
});
});
describe('Semantic Consistency', () => {
test('color matches semantic meaning', async ({ page }) => {
await page.goto('/components/molecules/status-indicator');
// Success = green
const success = page.locator('[data-testid="status-success"]');
const successDot = success.locator('.status-dot');
const successBg = await successDot.evaluate(el =>
getComputedStyle(el).backgroundColor
);
expect(successBg).toBe('rgb(16, 185, 129)'); // --green-500
// Warning = yellow
const warning = page.locator('[data-testid="status-warning"]');
const warningDot = warning.locator('.status-dot');
const warningBg = await warningDot.evaluate(el =>
getComputedStyle(el).backgroundColor
);
expect(warningBg).toBe('rgb(245, 158, 11)'); // --yellow-500
});
});
describe('Accessibility', () => {
test('screenreader reads both dot and text', async ({ page }) => {
await page.goto('/components/molecules/status-indicator');
const indicator = page.locator('[data-testid="status-indicator"]');
const text = await indicator.textContent();
expect(text).toContain('Active');
// Dot has role
const dot = indicator.locator('.status-dot');
const role = await dot.getAttribute('role');
expect(role).toBe('img');
});
});
});
ORGANISM TESTS
Card Test Suite
describe('Card Organism', () => {
describe('Structure Tests', () => {
test('card has semantic HTML', async ({ page }) => {
await page.goto('/components/organisms/card');
const card = page.locator('[data-testid="card"]');
// Should be article or section
const tagName = await card.evaluate(el => el.tagName.toLowerCase());
expect(['article', 'section']).toContain(tagName);
});
test('card has proper heading hierarchy', async ({ page }) => {
await page.goto('/components/organisms/card');
const card = page.locator('[data-testid="card"]');
// Has h3 (assuming card in h2 section)
const heading = card.locator('h3');
await expect(heading).toBeVisible();
});
});
describe('Interaction Tests', () => {
test('clickable card responds to click', async ({ page }) => {
await page.goto('/components/organisms/card?clickable=true');
const card = page.locator('[data-testid="card-clickable"]');
let clicked = false;
await page.exposeFunction('handleCardClick', () => { clicked = true; });
await page.evaluate(() => {
document.querySelector('[data-testid="card-clickable"]')
.addEventListener('click', () => window.handleCardClick());
});
await card.click();
expect(clicked).toBe(true);
});
test('card shows hover state', async ({ page }) => {
await page.goto('/components/organisms/card');
const card = page.locator('[data-testid="card"]');
// Get initial shadow
const initialShadow = await card.evaluate(el =>
getComputedStyle(el).boxShadow
);
// Hover
await card.hover();
// Get hover shadow
const hoverShadow = await card.evaluate(el =>
getComputedStyle(el).boxShadow
);
// Shadow should change
expect(hoverShadow).not.toBe(initialShadow);
});
});
describe('Content Tests', () => {
test('card renders all sections', async ({ page }) => {
await page.goto('/components/organisms/card');
const card = page.locator('[data-testid="card"]');
// Header
const header = card.locator('.card-header');
await expect(header).toBeVisible();
// Content
const content = card.locator('.card-content');
await expect(content).toBeVisible();
// Footer
const footer = card.locator('.card-footer');
await expect(footer).toBeVisible();
});
test('card molecules are properly composed', async ({ page }) => {
await page.goto('/components/organisms/card');
const card = page.locator('[data-testid="card"]');
// Status indicator molecule
const statusIndicator = card.locator('.status-indicator');
await expect(statusIndicator).toBeVisible();
// Progress indicator molecule
const progressIndicator = card.locator('.progress-indicator');
await expect(progressIndicator).toBeVisible();
});
});
});
INTEGRATION TESTS
Dashboard Page Test Suite
describe('Dashboard Integration', () => {
test('page loads with all organisms', async ({ page }) => {
await page.goto('/dashboard');
// Navbar present
const navbar = page.locator('[data-testid="navbar"]');
await expect(navbar).toBeVisible();
// Page header present
const header = page.locator('.page-header');
await expect(header).toBeVisible();
// Card grid present
const grid = page.locator('.card-grid');
await expect(grid).toBeVisible();
// Cards present
const cards = page.locator('.card');
const count = await cards.count();
expect(count).toBeGreaterThan(0);
});
test('user can navigate from dashboard to detail', async ({ page }) => {
await page.goto('/dashboard');
// Click first card
const firstCard = page.locator('.card').first();
await firstCard.click();
// Should navigate to detail view
await page.waitForURL(/.*\/projects\/\d+/);
// Detail view loaded
const detailView = page.locator('.detail-layout');
await expect(detailView).toBeVisible();
});
test('patterns compose correctly', async ({ page }) => {
await page.goto('/dashboard');
// Card contains proper molecule hierarchy
const card = page.locator('.card').first();
// Status indicator in card header
const statusInHeader = card.locator('.card-header .status-indicator');
await expect(statusInHeader).toBeVisible();
// Progress indicator in card footer
const progressInFooter = card.locator('.card-footer .progress-indicator');
await expect(progressInFooter).toBeVisible();
});
test('page is responsive', async ({ page }) => {
await page.goto('/dashboard');
// Desktop: 4 column grid
await page.setViewportSize({ width: 1440, height: 900 });
const desktopGrid = page.locator('.card-grid');
const desktopColumns = await desktopGrid.evaluate(el =>
getComputedStyle(el).gridTemplateColumns.split(' ').length
);
expect(desktopColumns).toBeGreaterThanOrEqual(3);
// Mobile: 1 column
await page.setViewportSize({ width: 375, height: 667 });
const mobileColumns = await desktopGrid.evaluate(el =>
getComputedStyle(el).gridTemplateColumns.split(' ').length
);
expect(mobileColumns).toBe(1);
});
test('loading states work', async ({ page }) => {
// Mock slow API
await page.route('**/api/projects', route => {
setTimeout(() => route.fulfill({ body: '[]' }), 2000);
});
await page.goto('/dashboard');
// Skeleton cards visible
const skeletons = page.locator('.skeleton-card');
await expect(skeletons.first()).toBeVisible();
// Wait for loading
await page.waitForTimeout(2500);
// Real content visible
const cards = page.locator('.card');
await expect(cards.first()).toBeVisible();
});
});
ACCESSIBILITY TEST SUITE
Comprehensive A11y Tests
import { injectAxe, checkA11y } from 'axe-playwright';
describe('Accessibility Compliance', () => {
test('entire dashboard passes WCAG 2.1 AA', async ({ page }) => {
await page.goto('/dashboard');
await injectAxe(page);
const results = await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: { html: true }
});
expect(results.violations).toHaveLength(0);
});
test('keyboard navigation works throughout page', async ({ page }) => {
await page.goto('/dashboard');
// Tab through all focusable elements
const focusable = await page.locator(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const count = await focusable.count();
for (let i = 0; i < count; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluate(() => document.activeElement.tagName);
expect(focused).toBeTruthy();
}
});
test('screen reader landmarks present', async ({ page }) => {
await page.goto('/dashboard');
// Main navigation
const nav = page.locator('nav[role="navigation"]');
await expect(nav).toBeVisible();
// Main content
const main = page.locator('main');
await expect(main).toBeVisible();
// Landmarks have labels
const navLabel = await nav.getAttribute('aria-label');
expect(navLabel).toBeTruthy();
});
test('heading hierarchy is correct', async ({ page }) => {
await page.goto('/dashboard');
// Get all headings
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
const levels = await Promise.all(
headings.map(h => h.evaluate(el => parseInt(el.tagName[1])))
);
// Should start with h1
expect(levels[0]).toBe(1);
// No skipped levels
for (let i = 1; i < levels.length; i++) {
const diff = levels[i] - levels[i - 1];
expect(diff).toBeLessThanOrEqual(1);
}
});
test('images have alt text', async ({ page }) => {
await page.goto('/dashboard');
const images = await page.locator('img').all();
for (const img of images) {
const alt = await img.getAttribute('alt');
expect(alt).not.toBeNull();
}
});
test('form fields have labels', async ({ page }) => {
await page.goto('/form');
const inputs = await page.locator('input, select, textarea').all();
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledby = await input.getAttribute('aria-labelledby');
if (id) {
const label = page.locator(`label[for="${id}"]`);
const hasLabel = await label.count() > 0;
expect(hasLabel || ariaLabel || ariaLabelledby).toBe(true);
} else {
expect(ariaLabel || ariaLabelledby).toBeTruthy();
}
}
});
test('color is not the only visual means', async ({ page }) => {
await page.goto('/dashboard');
// Status indicators have both color AND text/icon
const statusIndicators = await page.locator('.status-indicator').all();
for (const indicator of statusIndicators) {
const text = await indicator.textContent();
expect(text.trim().length).toBeGreaterThan(0);
}
});
});
VISUAL REGRESSION TEST SUITE
Automated Screenshot Comparison
import { test, expect } from '@playwright/test';
describe('Visual Regression', () => {
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1440, height: 900 }
];
for (const viewport of viewports) {
test(`dashboard ${viewport.name}`, async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot(`dashboard-${viewport.name}.png`, {
fullPage: true,
maxDiffPixelRatio: 0.01
});
});
}
test('component states', async ({ page }) => {
await page.goto('/components');
// Button states
const btnPrimary = page.locator('[data-testid="btn-primary"]');
await expect(btnPrimary).toHaveScreenshot('btn-primary-default.png');
await btnPrimary.hover();
await expect(btnPrimary).toHaveScreenshot('btn-primary-hover.png');
await btnPrimary.focus();
await expect(btnPrimary).toHaveScreenshot('btn-primary-focus.png');
// Input states
const input = page.locator('[data-testid="input-text"]');
await expect(input).toHaveScreenshot('input-default.png');
await input.focus();
await expect(input).toHaveScreenshot('input-focus.png');
await input.fill('Test value');
await expect(input).toHaveScreenshot('input-filled.png');
});
test('dark theme', async ({ page }) => {
await page.goto('/dashboard');
await page.evaluate(() => {
document.documentElement.setAttribute('data-theme', 'dark');
});
await expect(page).toHaveScreenshot('dashboard-dark.png', {
fullPage: true
});
});
});
PERFORMANCE TEST SUITE
Loading and Rendering Performance
describe('Performance', () => {
test('dashboard loads within 2 seconds', async ({ page }) => {
const startTime = Date.now();
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(2000);
});
test('First Contentful Paint < 1s', async ({ page }) => {
await page.goto('/dashboard');
const fcp = await page.evaluate(() => {
const paint = performance.getEntriesByType('paint')
.find(entry => entry.name === 'first-contentful-paint');
return paint ? paint.startTime : 0;
});
expect(fcp).toBeLessThan(1000);
});
test('Cumulative Layout Shift < 0.1', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const cls = await page.evaluate(() => {
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
}).observe({ type: 'layout-shift', buffered: true });
return cls;
});
expect(cls).toBeLessThan(0.1);
});
test('card grid renders without jank', async ({ page }) => {
await page.goto('/dashboard');
const startTime = Date.now();
await page.waitForSelector('.card-grid .card:nth-child(10)');
const renderTime = Date.now() - startTime;
expect(renderTime).toBeLessThan(500);
});
});
TEST AUTOMATION SETUP
GitHub Actions Workflow
name: Design Pattern Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Upload coverage
uses: codecov/codecov-action@v3
accessibility-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run a11y tests
run: npm run test:a11y
- name: Upload report
if: always()
uses: actions/upload-artifact@v3
with:
name: a11y-report
path: test-results/
visual-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run visual tests
run: npm run test:visual
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshot-diffs
path: test-results/
Package.json Scripts
{
"H.P.004-SCRIPTS": {
"test": "npm run test:unit && npm run test:a11y && npm run test:visual",
"test:unit": "jest --coverage",
"test:a11y": "playwright test tests/accessibility/",
"test:visual": "playwright test tests/visual/",
"test:integration": "playwright test tests/integration/",
"test:watch": "jest --watch",
"test:update-snapshots": "playwright test --update-snapshots"
}
}
CONTINUOUS QUALITY MONITORING
Metrics Dashboard
// Generate test metrics
{
"coverage": {
"statements": 95.2,
"branches": 92.8,
"functions": 94.1,
"lines": 95.5
},
"accessibility": {
"violations": 0,
"warnings": 3,
"passes": 127
},
"visual": {
"snapshots": 156,
"updated": 0,
"failed": 0
},
"performance": {
"avgLoadTime": 1247,
"fcp": 823,
"cls": 0.042
}
}
Quality Gates
// Enforce minimum standards
const qualityGates = {
coverage: {
statements: 90,
branches: 85,
functions: 90,
lines: 90
},
accessibility: {
violations: 0,
wcagLevel: 'AA'
},
performance: {
loadTime: 2000,
fcp: 1000,
cls: 0.1
},
visual: {
maxDiffPixelRatio: 0.01
}
};
// Check before merge
function passesQualityGates(metrics) {
return (
metrics.coverage.statements >= qualityGates.coverage.statements &&
metrics.accessibility.violations === 0 &&
metrics.performance.loadTime <= qualityGates.performance.loadTime
);
}
This testing specification ensures every pattern in the library is production-ready, accessible, and maintainable. Run tests before every commit to maintain quality.