ui-testing
UI Testing Skill
Expert in UI testing with Cypress and Testing Library. For deep Playwright expertise, see the e2e-playwright skill.
Framework Selection Guide
| Framework | Best For | Key Strength |
|---|---|---|
| Playwright | E2E, cross-browser | Auto-wait, multi-browser → Use e2e-playwright skill |
| Cypress | E2E, developer experience | Time-travel debugging, real-time reload |
| Testing Library | Component tests | User-centric queries, accessibility-first |
1. Cypress (E2E Testing)
Why Cypress?
- Developer-friendly API
- Real-time reloading
- Time-travel debugging
- Screenshot/video recording
- Stubbing and mocking built-in
Basic Test
describe('User Authentication', () => {
it('should login with valid credentials', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('SecurePass123!');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('h1').should('have.text', 'Welcome, User');
});
it('should show error with invalid credentials', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('wrong@example.com');
cy.get('input[name="password"]').type('WrongPass');
cy.get('button[type="submit"]').click();
cy.get('.error-message')
.should('be.visible')
.and('have.text', 'Invalid credentials');
});
});
Custom Commands (Reusable Actions)
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
// Usage in tests
it('should display dashboard for logged-in user', () => {
cy.login('user@example.com', 'SecurePass123!');
cy.get('h1').should('have.text', 'Dashboard');
});
API Mocking with Intercept
it('should display mocked user data', () => {
cy.intercept('GET', '/api/user', {
statusCode: 200,
body: {
id: 1,
name: 'Mock User',
email: 'mock@example.com',
},
}).as('getUser');
cy.visit('/profile');
cy.wait('@getUser');
cy.get('.user-name').should('have.text', 'Mock User');
});
3. React Testing Library (Component Tests)
Why Testing Library?
- User-centric queries (accessibility-first)
- Encourages best practices (testing behavior, not implementation)
- Works with React, Vue, Svelte, Angular
Component Test Example
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('should render email and password inputs', () => {
render(<LoginForm />);
expect(screen.getByLabelText('Email')).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
});
it('should call onSubmit with email and password', async () => {
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Type into inputs
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'user@example.com' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'SecurePass123!' },
});
// Submit form
fireEvent.click(screen.getByRole('button', { name: /login/i }));
// Verify callback
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'SecurePass123!',
});
});
it('should show validation error for invalid email', async () => {
render(<LoginForm />);
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'invalid-email' },
});
fireEvent.blur(screen.getByLabelText('Email'));
expect(await screen.findByText('Invalid email format')).toBeInTheDocument();
});
});
User-Centric Queries (Preferred)
// ✅ GOOD: Accessible queries (user-facing)
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email');
screen.getByPlaceholderText('Enter your email');
screen.getByText('Welcome');
// ❌ BAD: Implementation-detail queries (fragile)
screen.getByClassName('btn-primary'); // Changes when CSS changes
screen.getByTestId('submit-button'); // Not user-facing
Test Strategies
1. Testing Pyramid
/\
/ \ E2E (10%)
/____\
/ \ Integration (30%)
/________\
/ \ Unit (60%)
/____________\
Unit Tests (60%):
- Individual components in isolation
- Fast, cheap, many tests
- Mock external dependencies
Integration Tests (30%):
- Multiple components working together
- API integration, data flow
- Moderate speed, moderate cost
E2E Tests (10%):
- Full user journeys (login → checkout)
- Slowest, most expensive
- Critical paths only
2. Test Coverage Strategy
What to Test:
- ✅ Happy paths (core user flows)
- ✅ Error states (validation, API failures)
- ✅ Edge cases (empty states, max limits)
- ✅ Accessibility (keyboard navigation, screen readers)
- ✅ Regression bugs (add test for each bug fix)
What NOT to Test:
- ❌ Third-party libraries (assume they work)
- ❌ Implementation details (internal state, CSS classes)
- ❌ Trivial code (getters, setters)
3. Flakiness Mitigation
Common Causes of Flaky Tests:
- Race Conditions
❌ Bad:
await page.click('button');
const text = await page.textContent('.result'); // May fail!
✅ Good:
await page.click('button');
await page.waitForSelector('.result'); // Wait for element
const text = await page.textContent('.result');
- Non-Deterministic Data
❌ Bad:
expect(page.locator('.user')).toHaveCount(5); // Depends on database state
✅ Good:
// Mock API to return deterministic data
await page.route('**/api/users', (route) =>
route.fulfill({
body: JSON.stringify([{ id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }]),
})
);
expect(page.locator('.user')).toHaveCount(2); // Predictable
- Timing Issues
❌ Bad:
await page.waitForTimeout(3000); // Arbitrary wait
✅ Good:
await page.waitForSelector('.loaded'); // Wait for specific condition
await page.waitForLoadState('networkidle'); // Wait for network idle
- Test Interdependence
❌ Bad:
test('create user', async () => {
// Creates user in DB
});
test('login user', async () => {
// Depends on previous test creating user
});
✅ Good:
test.beforeEach(async () => {
// Each test creates its own user
await createTestUser();
});
test.afterEach(async () => {
await cleanupTestUsers();
});
Accessibility Testing
1. Automated Accessibility Tests (axe-core)
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('should have no accessibility violations', async ({ page }) => {
await page.goto('https://example.com');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
2. Keyboard Navigation
test('should navigate form with keyboard', async ({ page }) => {
await page.goto('/form');
// Tab through form fields
await page.keyboard.press('Tab');
await expect(page.locator('input[name="email"]')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.locator('input[name="password"]')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.locator('button[type="submit"]')).toBeFocused();
// Submit with Enter
await page.keyboard.press('Enter');
await expect(page).toHaveURL('**/dashboard');
});
3. Screen Reader Testing (aria-label, roles)
test('should have proper ARIA labels', async ({ page }) => {
await page.goto('/login');
// Verify accessible names
await expect(page.getByRole('textbox', { name: 'Email' })).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Password' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
// Verify error announcements (aria-live)
await page.fill('input[name="email"]', 'invalid-email');
await page.click('button[type="submit"]');
const errorRegion = page.locator('[role="alert"]');
await expect(errorRegion).toHaveText('Invalid email format');
});
CI/CD Integration
1. GitHub Actions (Playwright)
name: E2E Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
test:
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: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
2. Parallel Execution
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined, // Parallel in CI
fullyParallel: true,
retries: process.env.CI ? 2 : 0, // Retry flaky tests in CI
reporter: process.env.CI ? 'github' : 'html',
});
3. Sharding (Large Test Suites)
# Split tests across 4 machines
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
Best Practices
1. Use Data Attributes for Stable Selectors
<!-- ✅ GOOD: Stable selector -->
<button data-testid="submit-button">Submit</button>
<!-- ❌ BAD: Fragile selectors -->
<button class="btn btn-primary">Submit</button> <!-- CSS changes break tests -->
// Test
await page.click('[data-testid="submit-button"]');
2. Test User Behavior, Not Implementation
❌ Bad:
// Testing internal state
expect(component.state.isLoading).toBe(true);
✅ Good:
// Testing visible UI
expect(screen.getByText('Loading...')).toBeInTheDocument();
3. Keep Tests Independent
// ✅ GOOD: Each test is independent
test.beforeEach(async ({ page }) => {
await page.goto('/');
await login(page, 'user@example.com', 'password');
});
test('test 1', async ({ page }) => {
// Fresh state
});
test('test 2', async ({ page }) => {
// Fresh state
});
4. Use Meaningful Assertions
❌ Bad:
expect(true).toBe(true); // Useless assertion
✅ Good:
await expect(page.locator('.success-message')).toHaveText(
'Order placed successfully'
);
5. Avoid Hard-Coded Waits
❌ Bad:
await page.waitForTimeout(5000); // Slow, brittle
✅ Good:
await page.waitForSelector('.results'); // Wait for specific element
await expect(page.locator('.results')).toBeVisible(); // Built-in wait
Debugging Tests
1. Headed Mode (See Browser)
npx playwright test --headed
npx playwright test --headed --debug # Pause on each step
2. Screenshot on Failure
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({ path: `failure-${testInfo.title}.png` });
}
});
3. Trace Viewer (Time-Travel Debugging)
// playwright.config.ts
export default defineConfig({
use: {
trace: 'on-first-retry', // Record trace on retry
},
});
# View trace
npx playwright show-trace trace.zip
4. Console Logs
page.on('console', (msg) => console.log('Browser log:', msg.text()));
page.on('pageerror', (error) => console.error('Page error:', error));
Common Patterns
1. Testing Forms
test('should validate form fields', async ({ page }) => {
await page.goto('/form');
// Empty submission (validation)
await page.click('button[type="submit"]');
await expect(page.locator('.email-error')).toHaveText('Email is required');
// Invalid email
await page.fill('input[name="email"]', 'invalid');
await page.click('button[type="submit"]');
await expect(page.locator('.email-error')).toHaveText('Invalid email format');
// Valid submission
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'SecurePass123!');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('**/success');
});
2. Testing Modals
test('should open and close modal', async ({ page }) => {
await page.goto('/');
// Open modal
await page.click('[data-testid="open-modal"]');
await expect(page.locator('.modal')).toBeVisible();
// Close with X button
await page.click('.modal .close-button');
await expect(page.locator('.modal')).not.toBeVisible();
// Open again, close with Escape
await page.click('[data-testid="open-modal"]');
await page.keyboard.press('Escape');
await expect(page.locator('.modal')).not.toBeVisible();
});
3. Testing Drag and Drop
test('should drag and drop items', async ({ page }) => {
await page.goto('/kanban');
const todoItem = page.locator('[data-testid="item-1"]');
const doneColumn = page.locator('[data-testid="column-done"]');
// Drag item from TODO to DONE
await todoItem.dragTo(doneColumn);
// Verify item moved
await expect(doneColumn.locator('[data-testid="item-1"]')).toBeVisible();
});
Resources
- Playwright Documentation
- Cypress Documentation
- Testing Library
- Web Content Accessibility Guidelines (WCAG)
Activation Keywords
Ask me about:
- "How to write E2E tests with Playwright"
- "Cypress test examples"
- "React Testing Library best practices"
- "Page Object Model for UI tests"
- "Accessibility testing with axe-core"
- "How to fix flaky tests"
- "CI/CD integration for UI tests"
- "Debugging Playwright tests"
- "Test automation strategies"
More from anton-abyzov/specweave
technical-writing
Technical writing expert for API documentation, README files, tutorials, changelog management, and developer documentation. Covers style guides, information architecture, versioning docs, OpenAPI/Swagger, and documentation-as-code. Activates for technical writing, API docs, README, changelog, tutorial writing, documentation, technical communication, style guide, OpenAPI, Swagger, developer docs.
45spec-driven-brainstorming
Spec-driven brainstorming and product discovery expert. Helps teams ideate features, break down epics, conduct story mapping sessions, prioritize using MoSCoW/RICE/Kano, and validate ideas with lean startup methods. Activates for brainstorming, product discovery, story mapping, feature ideation, prioritization, MoSCoW, RICE, Kano model, lean startup, MVP definition, product backlog, feature breakdown.
43kafka-architecture
Apache Kafka architecture expert for cluster design, capacity planning, and high availability. Use when designing Kafka clusters, choosing partition strategies, or sizing brokers for production workloads.
34docusaurus
Docusaurus 3.x documentation framework - MDX authoring, theming, versioning, i18n. Use for documentation sites or spec-weave.com.
29frontend
Expert frontend developer for React, Vue, Angular, and modern JavaScript/TypeScript. Use when creating components, implementing hooks, handling state management, or building responsive web interfaces. Covers React 18+ features, custom hooks, form handling, and accessibility best practices.
29reflect
Self-improving AI memory system that persists learnings across sessions in CLAUDE.md. Use when capturing corrections, remembering user preferences, or extracting patterns from successful implementations. Enables continual learning without starting from zero each conversation.
27