Back to skills
SKILL.md
Frontend Testing Fgarofalo56 Suppercharge Microso
ASecurityComprehensive frontend testing with Playwright, Cypress, Jest, and React Testing Library. Covers E2E testing, component testing, unit testing, visual regression, and accessibility testing. Use for quality assurance, CI/CD integration, and test automation.
- 666 stars
- 0 votes
- 0 copies
- 6 views
- Added February 8, 2026
Works with
Security analysis
96/100- Installs packages at runtime which could introduce malicious dependencies
npx -y skills add majiayu000/claude-skill-registry --skill frontend-testing-fgarofalo56-suppercharge-microso --agent claude-codeAre you the author of Frontend Testing Fgarofalo56 Suppercharge Microso?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/majiayu000-frontend-testing-fgarofalo56-suppercharge-microso)---
name: frontend-testing
description: Comprehensive frontend testing with Playwright, Cypress, Jest, and React Testing Library. Covers E2E testing, component testing, unit testing, visual regression, and accessibility testing. Use for quality assurance, CI/CD integration, and test automation.
---
# Frontend Testing
Complete testing strategies for React and frontend applications.
## Testing Pyramid
```
/\
/ \ E2E Tests (Playwright/Cypress)
/----\ - Critical user flows
/ \ - Cross-browser testing
/--------\ Integration Tests
/ \ - Component interactions
/------------\ Unit Tests (Jest/Vitest)
- Functions, hooks, utilities
```
## Unit Testing with Jest/Vitest
### Setup
```bash
# Jest
npm install -D jest @types/jest ts-jest @testing-library/react @testing-library/jest-dom
# Vitest (faster, ESM-native)
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
```
### Jest Configuration
```js
// jest.config.js
module.exports = {
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"\\.(css|less|scss)$": "identity-obj-proxy",
},
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
collectCoverageFrom: [
"src/**/*.{ts,tsx}",
"!src/**/*.d.ts",
"!src/**/*.stories.tsx",
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
```
### Testing Utilities
```tsx
// src/utils/format.ts
export function formatCurrency(amount: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
}
export function formatDate(date: Date | string): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
}
// src/utils/format.test.ts
import { formatCurrency, formatDate } from "./format";
describe("formatCurrency", () => {
it("formats USD correctly", () => {
expect(formatCurrency(1234.56)).toBe("$1,234.56");
});
it("formats EUR correctly", () => {
expect(formatCurrency(1234.56, "EUR")).toBe("€1,234.56");
});
it("handles zero", () => {
expect(formatCurrency(0)).toBe("$0.00");
});
it("handles negative numbers", () => {
expect(formatCurrency(-50)).toBe("-$50.00");
});
});
describe("formatDate", () => {
it("formats Date object", () => {
const date = new Date("2024-03-15");
expect(formatDate(date)).toBe("March 15, 2024");
});
it("formats ISO string", () => {
expect(formatDate("2024-03-15")).toBe("March 15, 2024");
});
});
```
### Testing Custom Hooks
```tsx
// src/hooks/useCounter.ts
import { useState, useCallback } from "react";
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
}
// src/hooks/useCounter.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("initializes with custom value", () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
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 to initial value", () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
```
## Component Testing with React Testing Library
### Basic Component Testing
```tsx
// src/components/Button.tsx
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
variant?: "primary" | "secondary" | "danger";
loading?: boolean;
}
export function Button({
children,
onClick,
disabled = false,
variant = "primary",
loading = false,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled || loading}
className={`btn btn-${variant}`}
aria-busy={loading}
>
{loading ? "Loading..." : children}
</button>
);
}
// src/components/Button.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";
describe("Button", () => {
it("renders children", () => {
render(<Button>Click me</Button>);
expect(
screen.getByRole("button", { name: /click me/i }),
).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const handleClick = jest.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
it("shows loading state", () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole("button")).toHaveTextContent("Loading...");
expect(screen.getByRole("button")).toBeDisabled();
expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true");
});
it("applies variant class", () => {
render(<Button variant="danger">Delete</Button>);
expect(screen.getByRole("button")).toHaveClass("btn-danger");
});
});
```
### Testing Forms
```tsx
// src/components/LoginForm.tsx
import { useState } from "react";
interface LoginFormProps {
onSubmit: (email: string, password: string) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
await onSubmit(email, password);
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} aria-label="Login form">
{error && (
<div role="alert" className="error">
{error}
</div>
)}
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
aria-describedby={error ? "error-message" : undefined}
/>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
/>
<button type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign In"}
</button>
</form>
);
}
// src/components/LoginForm.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
describe("LoginForm", () => {
const mockOnSubmit = jest.fn();
beforeEach(() => {
mockOnSubmit.mockReset();
});
it("renders all form fields", () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /sign in/i }),
).toBeInTheDocument();
});
it("submits form with valid data", async () => {
const user = userEvent.setup();
mockOnSubmit.mockResolvedValueOnce(undefined);
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText(/email/i), "test@example.com");
await user.type(screen.getByLabelText(/password/i), "password123");
await user.click(screen.getByRole("button", { name: /sign in/i }));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(
"test@example.com",
"password123",
);
});
});
it("shows loading state during submission", async () => {
const user = userEvent.setup();
mockOnSubmit.mockImplementation(() => new Promise(() => {})); // Never resolves
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText(/email/i), "test@example.com");
await user.type(screen.getByLabelText(/password/i), "password123");
await user.click(screen.getByRole("button", { name: /sign in/i }));
expect(screen.getByRole("button")).toHaveTextContent("Signing in...");
expect(screen.getByRole("button")).toBeDisabled();
});
it("displays error message on failure", async () => {
const user = userEvent.setup();
mockOnSubmit.mockRejectedValueOnce(new Error("Invalid credentials"));
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText(/email/i), "test@example.com");
await user.type(screen.getByLabelText(/password/i), "wrongpassword");
await user.click(screen.getByRole("button", { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent(
"Invalid credentials",
);
});
});
it("requires email field", async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText(/email/i)).toBeRequired();
});
it("requires password field", async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText(/password/i)).toBeRequired();
});
});
```
### Testing Async Components
```tsx
// src/components/UserProfile.tsx
import { useState, useEffect } from "react";
interface User {
id: string;
name: string;
email: string;
}
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUser() {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error("User not found");
const data = await res.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load");
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div aria-label="Loading">Loading...</div>;
if (error) return <div role="alert">{error}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// src/components/UserProfile.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { UserProfile } from "./UserProfile";
// Mock fetch globally
global.fetch = jest.fn();
describe("UserProfile", () => {
beforeEach(() => {
(fetch as jest.Mock).mockReset();
});
it("shows loading state initially", () => {
(fetch as jest.Mock).mockImplementation(() => new Promise(() => {}));
render(<UserProfile userId="1" />);
expect(screen.getByLabelText("Loading")).toBeInTheDocument();
});
it("displays user data on success", async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: "1",
name: "John Doe",
email: "john@example.com",
}),
});
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("john@example.com")).toBeInTheDocument();
});
});
it("displays error on failure", async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
});
render(<UserProfile userId="invalid" />);
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent("User not found");
});
});
});
```
## E2E Testing with Playwright
### Setup
```bash
npm init playwright@latest
```
### Configuration
```ts
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html"], ["junit", { outputFile: "test-results/junit.xml" }]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
{
name: "mobile-chrome",
use: { ...devices["Pixel 5"] },
},
{
name: "mobile-safari",
use: { ...devices["iPhone 12"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
```
### Page Objects Pattern
```ts
// e2e/pages/LoginPage.ts
import { Page, Locator } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.submitButton = page.getByRole("button", { name: "Sign In" });
this.errorMessage = page.getByRole("alert");
}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
```
### E2E Test Examples
```ts
// e2e/auth.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "./pages/LoginPage";
test.describe("Authentication", () => {
test("successful login redirects to dashboard", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "validpassword");
await expect(page).toHaveURL("/dashboard");
await expect(page.getByText("Welcome back")).toBeVisible();
});
test("invalid credentials show error", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "wrongpassword");
await loginPage.expectError("Invalid credentials");
await expect(page).toHaveURL("/login");
});
test("logout returns to home page", async ({ page }) => {
// Login first
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "validpassword");
// Now logout
await page.getByRole("button", { name: "Logout" }).click();
await expect(page).toHaveURL("/");
});
});
// e2e/dashboard.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Dashboard", () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto("/login");
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Password").fill("validpassword");
await page.getByRole("button", { name: "Sign In" }).click();
await page.waitForURL("/dashboard");
});
test("displays user stats", async ({ page }) => {
await expect(page.getByTestId("total-users")).toBeVisible();
await expect(page.getByTestId("revenue")).toBeVisible();
await expect(page.getByTestId("orders")).toBeVisible();
});
test("filters data by date range", async ({ page }) => {
await page.getByRole("button", { name: "Date Range" }).click();
await page.getByRole("option", { name: "Last 7 days" }).click();
// Wait for data to reload
await page.waitForResponse("**/api/stats*");
// Verify filter is applied
await expect(page.getByText("Last 7 days")).toBeVisible();
});
test("exports data to CSV", async ({ page }) => {
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export CSV" }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toContain(".csv");
});
});
```
### Visual Regression Testing
```ts
// e2e/visual.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Visual Regression", () => {
test("homepage matches snapshot", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png", {
fullPage: true,
maxDiffPixels: 100,
});
});
test("dashboard matches snapshot", async ({ page }) => {
// Login first
await page.goto("/login");
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Password").fill("password");
await page.getByRole("button", { name: "Sign In" }).click();
await page.waitForURL("/dashboard");
// Wait for all data to load
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot("dashboard.png", {
maxDiffPixels: 200,
});
});
test("components match snapshots", async ({ page }) => {
await page.goto("/components");
// Screenshot specific component
const button = page.getByTestId("primary-button");
await expect(button).toHaveScreenshot("primary-button.png");
const card = page.getByTestId("user-card");
await expect(card).toHaveScreenshot("user-card.png");
});
});
```
## Accessibility Testing
### With jest-axe
```tsx
// src/components/Form.test.tsx
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import { ContactForm } from "./ContactForm";
expect.extend(toHaveNoViolations);
describe("ContactForm accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContactForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
```
### With Playwright
```ts
// e2e/accessibility.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Accessibility", () => {
test("homepage has no violations", async ({ page }) => {
await page.goto("/");
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test("form has no violations", async ({ page }) => {
await page.goto("/contact");
const accessibilityScanResults = await new AxeBuilder({ page })
.include("#contact-form")
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test("keyboard navigation works", async ({ page }) => {
await page.goto("/");
// Tab through interactive elements
await page.keyboard.press("Tab");
await expect(page.getByRole("link", { name: "Home" })).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.getByRole("link", { name: "About" })).toBeFocused();
// Press Enter to activate
await page.keyboard.press("Enter");
await expect(page).toHaveURL("/about");
});
});
```
## CI/CD Integration
### GitHub Actions
```yaml
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node- cache: "npm"
- run: npm ci
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node- cache: "npm"
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
```
## Best Practices
1. **Test Behavior, Not Implementation** - Focus on what users see and do
2. **Use Accessible Queries** - getByRole, getByLabelText, getByText
3. **Avoid Test IDs When Possible** - Use semantic queries first
4. **Mock External Dependencies** - API calls, timers, etc.
5. **Keep Tests Isolated** - Each test should be independent
6. **Test Edge Cases** - Error states, empty states, loading
7. **Run Tests in CI** - Catch regressions early
8. **Maintain Test Coverage** - Aim for 80%+ on critical paths
## When to Use
- Unit Tests: Functions, hooks, utilities, pure logic
- Component Tests: UI components, forms, interactions
- Integration Tests: Component combinations, API integration
- E2E Tests: Critical user flows, cross-browser testing
- Visual Tests: Design consistency, responsive layouts
- Accessibility Tests: WCAG compliance, keyboard navigation
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.