Frontend Testing Skill
Description
Specialized agent for implementing meaningful tests in the React frontend following TDD principles and Testing Library best practices.
Context
- Project: kokkai-join frontend (React 19, TypeScript, Vite, React Router, Supabase)
- Testing framework: Vitest + React Testing Library
- State management: React Context API (AuthContext)
- Routing: React Router
- Focus: Testing user behavior and component interactions
When to Use This Skill
- Implementing new React components or features
- Writing tests for existing untested components
- Refactoring tests that rely on implementation details
- Adding integration tests for user flows
Testing Principles (MUST FOLLOW)
1. TDD Approach is MANDATORY
- RED: Write a failing test first
- GREEN: Write minimum code to pass
- REFACTOR: Improve code quality
2. Testing Library Philosophy
Guiding Principle: "The more your tests resemble the way your software is used, the more confidence they can give you."
✅ GOOD Tests
- Test from the user's perspective
- Query elements by accessible roles, labels, text
- Test behavior and interactions, not implementation
- Avoid testing internal state or methods
- Use
userEventfor realistic interactions
❌ BAD Tests
- Test implementation details (CSS classes, internal state)
- Query by test IDs excessively
- Mock everything (contexts, hooks, components)
- Test component internal methods
- Rely on component structure/nesting
3. Test Types and Guidelines
Component Tests
- Purpose: Test individual components in isolation
- Good examples:
SearchForm.test.tsx,HistoryDetailPage.test.tsx - What to test: User interactions, conditional rendering, prop handling, error states
- What NOT to test: CSS classes, internal state variables, component lifecycle details
Integration Tests
- Purpose: Test multiple components working together
- What to test: User flows, navigation, data passing between components
- What to mock: External APIs, authentication services
Page Tests
- Purpose: Test entire page behavior
- What to test: Data loading, error handling, navigation, user scenarios
- Mock strategically: API calls, auth state (not routing/components)
4. Specific Rules for This Project
General Rules
❌ NEVER USE DYNAMIC IMPORTS IN TESTS:
// BAD: Dynamic imports are prohibited
const fs = await import("node:fs")
const path = await import("node:path")
// GOOD: Use static imports at the top of the file
import { readFileSync } from "node:fs"
import { join } from "node:path"
✅ FIXTURE FILES:
- Place test data files in
frontend/fixtures/directory - Use static imports to read fixture files
- Example:
readFileSync(join(process.cwd(), "fixtures", "chunks.txt"), "utf-8")
✅ TEST EXECUTION:
- Always use
npm testto run frontend tests - Do NOT use
bun test- it will not work correctly
When Testing Components with Routing
✅ DO THIS:
import { MemoryRouter, Route, Routes } from "react-router-dom"
render(
<MemoryRouter initialEntries={["/histories/test-id"]}>
<Routes>
<Route path="/histories/:id" element={<HistoryDetailPage />} />
</Routes>
</MemoryRouter>
)
❌ DON'T DO THIS:
// Bad: Mocking useNavigate and testing mock calls
const mockNavigate = vi.fn()
vi.mock("react-router-dom", () => ({ useNavigate: () => mockNavigate }))
expect(mockNavigate).toHaveBeenCalledWith("/path")
BETTER: Test actual navigation by checking rendered content after interaction.
When Testing Components with Auth Context
✅ GOOD APPROACH:
// Mock auth context with realistic values
vi.mock("@/features/auth/contexts/AuthContext", () => ({
useAuth: () => ({
user: { id: "test-user", email: "test@example.com" },
signOut: vi.fn(),
loading: false
})
}))
// Then test component behavior
const logoutButton = screen.getByRole("button", { name: "ログアウト" })
await user.click(logoutButton)
// Verify what happens after logout (navigation, state change, etc.)
❌ AVOID:
// Bad: Testing CSS classes or implementation details
expect(container.querySelector(".auth-header")).toBeInTheDocument()
expect(button).toHaveClass("submit-button")
When Testing API Interactions
✅ GOOD APPROACH:
// Mock fetch at the global level
const mockFetch = vi.fn()
global.fetch = mockFetch
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: mockData })
})
render(<MyComponent />)
await waitFor(() => {
expect(screen.getByText(/Expected content from API/)).toBeInTheDocument()
})
❌ AVOID:
// Bad: Mocking Supabase client internals
vi.mock("@/lib/supabaseClient", () => ({
supabase: {
from: vi.fn().mockReturnValue({
select: vi.fn().mockReturnValue({...})
})
}
}))
5. Query Priority (Testing Library Best Practices)
Use queries in this order of preference:
Accessible to everyone (BEST):
getByRole:screen.getByRole("button", { name: "検索" })getByLabelText:screen.getByLabelText("Email")getByPlaceholderText:screen.getByPlaceholderText("検索キーワードを入力")getByText:screen.getByText("読み込み中...")
Semantic queries:
getByAltText: For imagesgetByTitle: For elements with title attribute
Test IDs (LAST RESORT):
getByTestId: Only when other queries are impractical- Avoid in favor of accessible queries
6. Common Patterns
Testing User Interactions
import userEvent from "@testing-library/user-event"
it("should submit form when user fills input and clicks submit", async () => {
const user = userEvent.setup()
const
render(<SearchForm />)
const input = screen.getByPlaceholderText(/検索キーワードを入力/)
const submitButton = screen.getByRole("button", { name: "検索" })
await user.type(input, "防衛費")
await user.click(submitButton)
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ query: "防衛費" })
)
})
})
Testing Loading States
it("should display loading state initially", () => {
mockFetch.mockImplementation(() => new Promise(() => {})) // Never resolves
render(<MyComponent />)
expect(screen.getByText("読み込み中...")).toBeInTheDocument()
})
it("should display data after loading", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData
})
render(<MyComponent />)
await waitFor(() => {
expect(screen.queryByText("読み込み中...")).not.toBeInTheDocument()
})
expect(screen.getByText(/Expected data/)).toBeInTheDocument()
})
Testing Error States
it("should display error message on API failure", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network error"))
render(<MyComponent />)
await waitFor(() => {
expect(screen.getByText("Network error")).toBeInTheDocument()
})
})
it("should display error for 404 response", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
json: async () => ({ message: "Not found" })
})
render(<MyComponent />)
await waitFor(() => {
expect(screen.getByText("Not found")).toBeInTheDocument()
})
})
Testing Conditional Rendering
it("should show login button when user is not authenticated", () => {
vi.mock("@/features/auth/contexts/AuthContext", () => ({
useAuth: () => ({ user: null, loading: false })
}))
render(<Header />)
expect(screen.getByRole("button", { name: "ログイン" })).toBeInTheDocument()
expect(screen.queryByRole("button", { name: "ログアウト" })).not.toBeInTheDocument()
})
Testing Navigation
it("should navigate to history detail page when item is clicked", async () => {
const user = userEvent.setup()
render(
<MemoryRouter initialEntries={["/histories"]}>
<Routes>
<Route path="/histories" element={<HistoryListPage />} />
<Route path="/histories/:id" element={<HistoryDetailPage />} />
</Routes>
</MemoryRouter>
)
const historyItem = screen.getByText("テスト検索クエリ")
await user.click(historyItem)
// Verify navigation by checking if detail page content is rendered
await waitFor(() => {
expect(screen.getByText(/検索日時:/)).toBeInTheDocument()
})
})
7. Error Cases to Avoid
❌ Testing Implementation Details
// BAD: Testing internal state
expect(component.state.isLoading).toBe(true)
// BAD: Testing CSS classes
expect(button).toHaveClass("submit-button")
// BAD: Using container.querySelector
expect(container.querySelector(".auth-header")).toBeInTheDocument()
❌ Mocking Too Much
// BAD: Mocking React Router when you should test actual routing
vi.mock("react-router-dom", () => ({
useNavigate: () => mockNavigate,
useLocation: () => ({ pathname: "/test" })
}))
// BETTER: Use MemoryRouter and test actual navigation
❌ Vague Assertions
// BAD: Doesn't verify what's actually rendered
expect(screen.getByText(/./)).toBeInTheDocument()
// BETTER: Test specific content
expect(screen.getByText("検索結果がありません")).toBeInTheDocument()
Implementation Workflow
Step 1: Analyze Requirements
- Understand the user interaction or feature
- Identify what the user should see/do
- Determine component dependencies (routing, auth, APIs)
Step 2: Write Failing Test (RED)
- Write test describing user behavior
- Use accessible queries (role, label, text)
- Run test to confirm it fails
- Ensure failure message is clear
Step 3: Implement Minimum Code (GREEN)
- Write simplest code to make test pass
- Don't over-engineer UI or logic
- Run test to confirm it passes
Step 4: Refactor (if needed)
- Improve component structure
- Extract reusable logic
- Ensure all tests still pass
Step 5: Add Edge Cases
- Test loading states
- Test error states
- Test empty states
- Test disabled states
- Test form validation
Step 6: Verify Test Quality
- Ask: "Does this test describe user behavior?"
- Ask: "Can I refactor component without breaking this test?"
- Ask: "Would this test catch real bugs?"
Test Setup Utilities
Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
clear: () => { store = {} }
}
})()
Object.defineProperty(globalThis, "localStorage", {
value: localStorageMock,
writable: true,
configurable: true
})
Mock global fetch
const mockFetch = vi.fn()
global.fetch = mockFetch
beforeEach(() => {
mockFetch.mockReset()
})
Mock Supabase Auth
vi.mock("@/lib/supabaseClient", () => ({
supabase: {
auth: {
getSession: vi.fn().mockResolvedValue({
data: { session: { access_token: "test-token" } },
error: null
})
}
}
}))
Output Format
When implementing tests, provide:
- Test file path: Where the test should be created
- Test description: What user behavior is being tested
- Test code: Following Testing Library best practices
- Run instructions: How to run the test
- Explanation: Why this test is meaningful and user-focused
Important Reminders
- TEST user behavior, not implementation details
- QUERY by role/label/text, not CSS classes or test IDs
- USE
userEventfor realistic user interactions - MOCK strategically (APIs, auth), not everything (routing, components)
- WAIT for async changes with
waitFor - WRITE failing test first (RED), then implement (GREEN), then refactor
- VERIFY tests describe what users do, not how code works
Examples of Meaningful Tests to Reference
- ✅
frontend/src/features/search/components/SearchForm.test.tsx- User interaction testing - ✅
frontend/src/features/history/pages/HistoryDetailPage.test.tsx- Page behavior testing - ⚠️
frontend/src/shared/components/AppHeader.test.tsx- AVOID CSS class testing pattern