xiaohongshu
xiaohongshu
frontend-testing
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.
Full skill instructions
Complete testing strategies for React and frontend applications.
/\
/ \ E2E Tests (Playwright/Cypress)
/----\ - Critical user flows
/ \ - Cross-browser testing
/--------\ Integration Tests
/ \ - Component interactions
/------------\ Unit Tests (Jest/Vitest)
- Functions, hooks, utilities
# 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.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,
},
},
};
// 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");
});
});
// 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);
});
});
// 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");
});
});
// 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), "[email protected]");
await user.type(screen.getByLabelText(/password/i), "password123");
await user.click(screen.getByRole("button", { name: /sign in/i }));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(
"[email protected]",
"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), "[email protected]");
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), "[email protected]");
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();
});
});
// 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: "[email protected]",
}),
});
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("[email protected]")).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");
});
});
});
npm init playwright@latest
// 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,
},
});
// 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/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("[email protected]", "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("[email protected]", "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("[email protected]", "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("[email protected]");
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");
});
});
// 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("[email protected]");
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");
});
});
// 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();
});
});
// 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");
});
});
# .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
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience