Component Testing
Our design system uses Playwright Component Testing to ensure robust, reliable component behavior. This guide covers our testing approach, powered by the Playwright MCP (Model Context Protocol) for intelligent test generation and execution.
Overview
Playwright Component Testing provides:
- Fast execution with real browser rendering
- Comprehensive coverage of user interactions
- Accessibility testing built-in
- Visual regression testing capabilities
- Cross-browser compatibility testing
Test Architecture
Test Structure
tests/
├── components/
│ ├── 1-component-test-initial-prompt.md # AI test generation prompt
│ ├── Button.spec.ts # Component tests
│ ├── Modal.spec.ts
│ └── ...
└── e2e/
└── a11y/ # End-to-end accessibility testsConfiguration
Our Playwright Component Testing is configured in playwright-ct.config.ts:
export default defineConfig({
testDir: './tests/components',
fullyParallel: true,
reporter: 'html',
use: {
trace: 'on-first-retry',
ctTemplateDir: 'playwright',
ctPort: 3100,
ctViteConfig: {
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});AI-Powered Test Generation
Playwright MCP Integration
We use the Playwright MCP (Model Context Protocol) to generate comprehensive component tests. This AI-powered approach ensures:
- Complete coverage of all component features
- Consistent testing patterns across components
- Intelligent test case generation based on component analysis
- Accessibility-first testing approach
- Lint-compliant code generation following project standards
Initial Prompt System
Our test generation uses the initial prompt located in tests/components/1-component-test-initial-prompt.md. This prompt provides:
- Comprehensive testing requirements
- Vue 3 specific patterns
- Accessibility testing guidelines
- Linting and code quality standards
- Error handling strategies
- Best practices and conventions
Using the AI Test Generator
To generate tests for a new component using the MCP system:
Model Requirement
Use Claude Sonnet 4+ as your AI model when generating component tests. This ensures optimal test quality, comprehensive coverage, and adherence to our testing patterns and Vue 3 best practices.
# Provide component information in this format:
<<<COMPONENT
[Vue SFC source code here]
COMPONENT>>>
<<<STRUCTURE
[Folder structure from src/ root]
STRUCTURE>>>
<<<RELATED
[Optional: Related composables, types, or components]
RELATED>>>The AI will analyze your component and generate comprehensive tests covering:
- Rendering and mounting
- Props validation and combinations
- Event emissions
- Slot functionality
- Accessibility requirements
- Error handling
- User interactions
- Code quality and linting compliance
Test Coverage Requirements
Core Testing Areas
1. Rendering Tests
test('should render component with default props', async ({ mount }) => {
const component = await mount(Button);
await expect(component).toBeVisible();
});2. Props Testing
test('should apply size variants correctly', async ({ mount }) => {
const component = await mount(Button, { props: { size: 'large' } });
await expect(component).toHaveClass(/spr-size-large/);
});3. Event Testing
test('should emit click event with correct payload', async ({ mount }) => {
let clickEvent: any;
const component = await mount(Button, {
on: {
click: (event) => {
clickEvent = event;
},
},
});
await component.click();
expect(clickEvent).toBeTruthy();
});4. Accessibility Testing
test('should have proper ARIA attributes', async ({ mount }) => {
const component = await mount(Button, { props: { disabled: true } });
await expect(component).toHaveAttribute('aria-disabled', 'true');
});
test('should be keyboard navigable', async ({ mount, page }) => {
await mount(Button);
await page.keyboard.press('Tab');
await expect(page.getByRole('button')).toBeFocused();
});5. Slot Testing
test('should render slot content correctly', async ({ mount }) => {
const component = await mount(Button, {
slots: { default: 'Custom Button Text' },
});
await expect(component).toContainText('Custom Button Text');
});Advanced Testing Scenarios
Conditional Rendering
test('should conditionally render elements based on props', async ({ mount }) => {
const component = await mount(Modal, { props: { showHeader: false } });
await expect(component.locator('.modal-header')).not.toBeVisible();
});Form Integration
test('should integrate with form validation', async ({ mount }) => {
const component = await mount(Input, {
props: { required: true, value: '' },
});
await component.blur();
await expect(component).toHaveAttribute('aria-invalid', 'true');
});Theme and Styling
test('should apply theme variants', async ({ mount }) => {
const component = await mount(Button, {
props: { variant: 'primary' },
});
await expect(component).toHaveClass(/spr-variant-primary/);
});Best Practices
Code Quality and Linting
Before running tests, ensure your test files meet code quality standards:
# Check lint issues in test files
npm run lintKey linting requirements for test files:
- Consistent code formatting and style
- Proper TypeScript typing
- ESLint rule compliance
- Import statement organization
- Consistent naming conventions
Selector Strategy (Priority Order)
- Role-based selectors (preferred):
page.getByRole('button', { name: 'Submit' });
page.getByRole('textbox', { name: 'Email' });- Text content selectors:
page.getByText('Click me');
page.getByLabel('Email address');- Test IDs (when needed):
page.getByTestId('submit-btn');Writing Maintainable Tests
Use Descriptive Test Names
// ✅ Good
test('should disable button and prevent clicks when disabled prop is true');
// ❌ Bad
test('disabled test');Group Related Tests
test.describe('Button Component', () => {
test.describe('Props', () => {
test('should render with default size');
test('should apply custom size variants');
});
test.describe('Events', () => {
test('should emit click events');
test('should prevent events when disabled');
});
});Avoid Timing Issues
// ✅ Good - Wait for specific conditions
await expect(modal).toBeVisible();
// ❌ Bad - Arbitrary timeouts
await page.waitForTimeout(500);Running Tests
Local Development
# Run lint checks on test files (recommended before running tests)
npm run lint
# Run all component tests
npm run test:components
# Run specific component test (short name)
npx playwright test Button.spec.ts --config=playwright-ct.config.ts
# Run specific test file (full path)
npx playwright test tests/components/Button.spec.ts --config=playwright-ct.config.ts
# Run with UI mode for debugging
npx playwright test --ui --config=playwright-ct.config.ts
# Generate test report
npx playwright show-reportDebugging Tests
Visual Debugging
// Add to test for visual debugging
await page.pause();Trace Viewer
# Run test with trace
npx playwright test --trace on
# View trace
npx playwright show-trace trace.zipScreenshots on Failure
test('should render correctly', async ({ mount }, testInfo) => {
const component = await mount(Button);
// Take screenshot on failure
if (testInfo.retry > 0) {
await testInfo.attach('screenshot', {
body: await page.screenshot(),
contentType: 'image/png',
});
}
});Component Test Examples
Simple Component Test
import { test, expect } from '@playwright/experimental-ct-vue';
import Button from '@/components/button/button.vue';
test.describe('Button Component', () => {
test('should render with default props', async ({ mount }) => {
const component = await mount(Button);
await expect(component).toBeVisible();
await expect(component).toHaveClass(/spr-button/);
});
test('should handle click events', async ({ mount }) => {
let clicked = false;
const component = await mount(Button, {
on: {
click: () => {
clicked = true;
},
},
});
await component.click();
expect(clicked).toBe(true);
});
});Complex Component Test
import { test, expect } from '@playwright/experimental-ct-vue';
import Modal from '@/components/modal/modal.vue';
test.describe('Modal Component', () => {
test('should manage focus correctly', async ({ mount, page }) => {
const component = await mount(Modal, {
props: {
modelValue: true,
title: 'Test Modal',
},
});
// Should focus the modal
await expect(component).toBeFocused();
// Should trap focus within modal
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
expect(['BUTTON', 'INPUT', 'A']).toContain(focusedElement);
});
test('should close on escape key', async ({ mount, page }) => {
let modelValue = true;
await mount(Modal, {
props: {
modelValue,
'onUpdate:modelValue': (value: boolean) => {
modelValue = value;
},
},
});
await page.keyboard.press('Escape');
expect(modelValue).toBe(false);
});
});Accessibility Testing
ARIA Attributes
test('should have proper ARIA attributes', async ({ mount }) => {
const component = await mount(Button, {
props: { disabled: true, 'aria-label': 'Submit form' },
});
await expect(component).toHaveAttribute('aria-disabled', 'true');
await expect(component).toHaveAttribute('aria-label', 'Submit form');
});Keyboard Navigation
test('should support keyboard navigation', async ({ mount, page }) => {
await mount(Dropdown);
// Open with Enter
await page.keyboard.press('Enter');
await expect(page.getByRole('listbox')).toBeVisible();
// Navigate with arrows
await page.keyboard.press('ArrowDown');
await expect(page.getByRole('option').first()).toBeFocused();
});Screen Reader Support
test('should provide screen reader announcements', async ({ mount, page }) => {
const component = await mount(Snackbar, {
props: { message: 'Success!', type: 'success' },
});
await expect(component).toHaveAttribute('role', 'alert');
await expect(component).toHaveAttribute('aria-live', 'polite');
});Integration with CI/CD
Our component tests run automatically in CI/CD pipelines:
# Azure Pipelines example
- task: Node.js
inputs:
command: 'custom'
customCommand: 'npm run lint'
workingDirectory: '$(System.DefaultWorkingDirectory)'
- task: Node.js
inputs:
command: 'custom'
customCommand: 'npm run test:components'
workingDirectory: '$(System.DefaultWorkingDirectory)'Both linting and tests must pass before code can be merged to main branches.
Getting Help
For component testing assistance:
- Check existing test examples in
tests/components/ - Use the AI test generator with the initial prompt
- Review the Playwright documentation for advanced patterns
- Ask the team for component-specific testing strategies
Pro Tip
Use the Playwright MCP system to generate comprehensive tests quickly. The AI understands our component patterns and will create tests that follow our conventions and cover all necessary scenarios.
Important
Always run lint checks and tests locally before committing. Failed linting or tests will block CI/CD deployment and prevent merging pull requests.
# Recommended pre-commit workflow
npm run lint # Check code quality
npm run test:components # Run component tests