frontend-testing
About
This skill helps developers implement comprehensive frontend testing using tools like Jest, Vitest, React Testing Library, and Cypress. It is used for building robust test suites covering unit, integration, and end-to-end testing for UI components. The skill enables regression prevention and supports test-driven development for quality assurance.
Quick Install
Claude Code
Recommended/plugin add https://github.com/aj-geddes/useful-ai-promptsgit clone https://github.com/aj-geddes/useful-ai-prompts.git ~/.claude/skills/frontend-testingCopy and paste this command in Claude Code to install this skill
Documentation
Frontend Testing
Overview
Build comprehensive test suites for frontend applications including unit tests, integration tests, and end-to-end tests with proper coverage and assertions.
When to Use
- Component testing
- Integration testing
- End-to-end testing
- Regression prevention
- Quality assurance
- Test-driven development
Implementation Examples
1. Jest Unit Testing (React)
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Button } from './Button';
describe('Button Component', () => {
it('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('disables button when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('applies variant styles correctly', () => {
const { container } = render(<Button variant="primary">Click</Button>);
const button = container.querySelector('button');
expect(button).toHaveClass('bg-blue-500');
});
it('applies size classes correctly', () => {
const { container } = render(<Button size="lg">Click</Button>);
const button = container.querySelector('button');
expect(button).toHaveClass('px-6 py-3 text-lg');
});
});
// hooks.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('resets count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
2. React Testing Library Integration Tests
// UserForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserForm } from './UserForm';
describe('UserForm Integration', () => {
beforeEach(() => {
// Clear mocks before each test
jest.clearAllMocks();
});
it('submits form with valid data', async () => {
const handleSubmit = jest.fn();
render(<UserForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText(/name/i), 'John Doe');
await userEvent.type(screen.getByLabelText(/email/i), 'john@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
});
});
});
it('displays validation errors for empty fields', async () => {
render(<UserForm onSubmit={jest.fn()} />);
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(screen.getByText(/name is required/i)).toBeInTheDocument();
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
});
it('displays validation error for invalid email', async () => {
render(<UserForm onSubmit={jest.fn()} />);
await userEvent.type(screen.getByLabelText(/email/i), 'invalid-email');
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
});
});
});
// UserList.test.tsx with data fetching
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
describe('UserList with API', () => {
beforeEach(() => {
jest.spyOn(global, 'fetch').mockClear();
});
it('displays loading state initially', () => {
(global.fetch as jest.Mock).mockImplementation(
() => new Promise(() => {}) // Never resolves
);
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('fetches and displays users', async () => {
const mockUsers = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' }
];
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockUsers
});
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('User 1')).toBeInTheDocument();
expect(screen.getByText('User 2')).toBeInTheDocument();
});
});
it('displays error message on fetch failure', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
3. Vitest for Vue Testing
// Button.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
describe('Button.vue', () => {
it('renders slot content', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me'
}
});
expect(wrapper.text()).toContain('Click me');
});
it('emits click event', async () => {
const wrapper = mount(Button);
await wrapper.trigger('click');
expect(wrapper.emitted('click')).toHaveLength(1);
});
it('disables button when disabled prop is true', () => {
const wrapper = mount(Button, {
props: { disabled: true }
});
expect(wrapper.attributes('disabled')).toBeDefined();
});
it('applies variant class', () => {
const wrapper = mount(Button, {
props: { variant: 'primary' }
});
expect(wrapper.classes()).toContain('bg-blue-500');
});
});
// composable.spec.ts
import { describe, it, expect } from 'vitest';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value', () => {
const { count } = useCounter();
expect(count.value).toBe(0);
});
it('increments count', () => {
const { count, increment } = useCounter();
increment();
expect(count.value).toBe(1);
});
});
4. Cypress E2E Testing
// cypress/e2e/login.cy.ts
describe('Login Flow', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/login');
});
it('logs in with valid credentials', () => {
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('h1').should('contain', 'Welcome');
});
it('displays error for invalid credentials', () => {
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('wrongpassword');
cy.get('button[type="submit"]').click();
cy.get('.error-message').should('contain', 'Invalid credentials');
});
it('validates email field', () => {
cy.get('input[name="email"]').type('invalid-email');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.get('.error-message').should('contain', 'Invalid email');
});
});
// cypress/e2e/user-management.cy.ts
describe('User Management', () => {
beforeEach(() => {
cy.login('admin@example.com', 'password123');
cy.visit('http://localhost:3000/users');
});
it('creates a new user', () => {
cy.get('button:contains("Add User")').click();
cy.get('input[name="name"]').type('New User');
cy.get('input[name="email"]').type('newuser@example.com');
cy.get('button[type="submit"]').click();
cy.get('.success-message').should('contain', 'User created');
cy.get('table tbody').should('contain', 'New User');
});
it('edits an existing user', () => {
cy.get('table tbody tr').first().contains('button', 'Edit').click();
cy.get('input[name="name"]').clear().type('Updated Name');
cy.get('button[type="submit"]').click();
cy.get('.success-message').should('contain', 'User updated');
});
it('deletes a user with confirmation', () => {
cy.get('table tbody tr').first().contains('button', 'Delete').click();
cy.get('.modal button:contains("Confirm")').click();
cy.get('.success-message').should('contain', 'User deleted');
});
});
// cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
cy.visit('http://localhost:3000/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');
});
5. Test Coverage Configuration
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/reportWebVitals.ts'
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: {
jsx: 'react-jsx'
}
}]
}
};
// package.json scripts
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"cypress": "cypress open",
"cypress:headless": "cypress run"
}
}
Best Practices
- Write tests alongside code (TDD)
- Test behavior, not implementation
- Use descriptive test names
- Keep tests focused and independent
- Mock external dependencies
- Aim for high coverage (>80%)
- Use semantic queries in React Testing Library
- Implement E2E tests for critical paths
- Test error scenarios
- Use CI/CD for automated testing
Resources
GitHub Repository
Related Skills
content-collections
MetaThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
creating-opencode-plugins
MetaThis skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
