Frontend Testing Guide
Complete testing documentation for the Framework M frontend application.
Table of Contents
- Overview
- Test Infrastructure
- Unit Tests
- Integration Tests
- E2E Tests
- Running Tests
- Writing New Tests
- Best Practices
- Troubleshooting
Overview
The frontend uses a comprehensive testing strategy with three layers:
- Unit Tests - Test individual components in isolation
- Integration Tests - Test component interactions and workflows
- E2E Tests - Test complete user workflows across browsers
Technology Stack
- Test Runner: Vitest - Vite-native test runner
- Component Testing: @testing-library/react - User-centric testing utilities
- User Interactions: @testing-library/user-event - Realistic user interactions
- Assertions: @testing-library/jest-dom - DOM matchers
- DOM Environment: happy-dom - Fast DOM implementation
- E2E Testing: Playwright - Cross-browser automation
Test Infrastructure
Configuration Files
vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "happy-dom",
globals: true,
setupFiles: ["./src/setupTests.ts"],
css: true,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
},
},
});
src/setupTests.ts
Global test setup that runs before all tests:
- Imports
@testing-library/jest-dommatchers - Mocks
window.matchMedia(for theme detection) - Mocks
localStorageandsessionStorage - Mocks
IntersectionObserverandResizeObserver - Auto-cleanup after each test
playwright.config.ts
E2E test configuration:
- Tests in
tests/e2e/directory - Runs against
http://localhost:5173 - Auto-starts dev server
- Tests on Chromium, Firefox, and WebKit
- Screenshots and videos on failure
Unit Tests
Location
Unit tests are colocated with components in __tests__ directories:
src/
├── components/
│ ├── __tests__/
│ │ ├── ThemeToggle.test.tsx
│ │ ├── AutoForm.test.tsx
│ │ └── AutoTable.test.tsx
│ ├── ThemeToggle.tsx
│ └── ...
Example: Component Test
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { ThemeToggle } from "../ThemeToggle";
import { ThemeProvider } from "../../contexts/ThemeContext";
describe("ThemeToggle", () => {
it("toggles theme when clicked", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>
);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("aria-label", "Switch to dark mode");
await user.click(button);
expect(button).toHaveAttribute("aria-label", "Switch to light mode");
expect(window.localStorage.getItem("framework-m-theme")).toBe("dark");
});
});
Covered Components
ThemeToggle.test.tsx (6 tests)
- ✅ Renders toggle button
- ✅ Shows correct icon based on theme
- ✅ Toggles theme on click
- ✅ Persists to localStorage
- ✅ Applies dark class to document
- ✅ Keyboard accessible
AutoForm.test.tsx (12 tests)
- ✅ Renders fields from JSON Schema
- ✅ Submit button visibility
- ✅ onChange/onSubmit callbacks
- ✅ Validation for required fields
- ✅ Pre-fills initial data
- ✅ Readonly and disabled modes
- ✅ Boolean fields as checkboxes
- ✅ Enum fields as selects
AutoTable.test.tsx (11 tests)
- ✅ Renders columns from schema
- ✅ Displays data from Refine useList
- ✅ Loading state
- ✅ Row selection
- ✅ onRowClick callback
- ✅ Column sorting
- ✅ Pagination controls
- ✅ Formats booleans, dates, nulls
Integration Tests
Location
Integration tests are in src/__tests__/integration/:
src/
└── __tests__/
└── integration/
├── crud-flow.test.tsx
└── theme-persistence.test.tsx
Example: CRUD Flow Test
describe("CRUD Flow Integration", () => {
it("completes full CRUD workflow", async () => {
// 1. READ: List view shows records
expect(screen.getByText("Test Todo 1")).toBeInTheDocument();
// 2. CREATE: Navigate to create form
await user.click(screen.getByText("Create New"));
await user.click(screen.getByText("Save"));
expect(mockDataProvider.create).toHaveBeenCalled();
// 3. UPDATE: Edit existing record
await user.click(screen.getByText("Test Todo 1"));
await user.click(screen.getByText("Update"));
expect(mockDataProvider.update).toHaveBeenCalled();
// 4. DELETE: Remove record
await user.click(screen.getByText("Delete"));
expect(mockDataProvider.deleteOne).toHaveBeenCalled();
});
});
Covered Workflows
crud-flow.test.tsx (3 tests)
- ✅ Full CRUD workflow (Create → Read → Update → Delete)
- ✅ Validation errors during create
- ✅ Network error handling
theme-persistence.test.tsx (7 tests)
- ✅ Theme persists to localStorage
- ✅ Restores theme on mount
- ✅ Applies dark class to document
- ✅ System preference detection
- ✅ Saved theme overrides system
- ✅ System preference change listener
- ✅ Theme consistency across components
E2E Tests
Location
E2E tests are in tests/e2e/:
tests/
└── e2e/
├── login.spec.ts
├── create-record.spec.ts
└── cross-browser.spec.ts
Example: Login Test
import { test, expect } from "@playwright/test";
test.describe("User Login", () => {
test("should successfully log in", async ({ page }) => {
await page.goto("/login");
await page.getByLabel(/email/i).fill("test@example.com");
await page.getByLabel(/password/i).fill("password123");
await page.getByRole("button", { name: /login/i }).click();
await expect(page).toHaveURL(/\/app\/dashboard/);
await expect(page.getByRole("button", { name: /logout/i })).toBeVisible();
});
});
Covered Scenarios
login.spec.ts (7 tests)
- ✅ Display login form
- ✅ Validation errors for empty form
- ✅ Error for invalid credentials
- ✅ Successful login
- ✅ Session persistence on reload
- ✅ Logout functionality
- ✅ Keyboard navigation
create-record.spec.ts (9 tests)
- ✅ Navigate to create form
- ✅ Display form fields from metadata
- ✅ Required field validation
- ✅ Create record successfully
- ✅ Cancel creation
- ✅ Network error handling
- ✅ Edit after creation
- ✅ Auto-save drafts
- ✅ Real-time validation
cross-browser.spec.ts (10 tests)
- ✅ Theme toggle across browsers
- ✅ Navigation between views
- ✅ Search functionality
- ✅ Bulk actions
- ✅ Form validation consistency
- ✅ Locale switching
- ✅ Responsive design (mobile)
- ✅ Keyboard shortcuts
- ✅ Page transitions
- ✅ Error states
Running Tests
Unit & Integration Tests
# Run all tests once
pnpm test --run
# Run tests in watch mode
pnpm test
# Run with UI
pnpm test:ui
# Run with coverage
pnpm test:coverage
E2E Tests
# Run all E2E tests
pnpm test:e2e
# Run with Playwright UI
pnpm test:e2e:ui
# Run in headed mode (see browser)
pnpm test:e2e:headed
# Run specific browser
pnpm test:e2e --project=chromium
pnpm test:e2e --project=firefox
pnpm test:e2e --project=webkit
# Run specific test file
pnpm test:e2e tests/e2e/login.spec.ts
CI/CD
Tests run automatically in CI:
# .github/workflows/test.yml
- name: Run unit tests
run: pnpm test --run
- name: Run E2E tests
run: pnpm test:e2e
Writing New Tests
Unit Test Template
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { YourComponent } from "../YourComponent";
describe("YourComponent", () => {
it("should do something", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<YourComponent onClick={handleClick} />);
const button = screen.getByRole("button");
await user.click(button);
expect(handleClick).toHaveBeenCalled();
});
});
Integration Test Template
import { describe, it, expect, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router";
import { Refine } from "@refinedev/core";
describe("Feature Integration", () => {
beforeEach(() => {
// Setup test environment
});
it("completes workflow", async () => {
render(
<BrowserRouter>
<Refine dataProvider={mockDataProvider}>
<YourFeature />
</Refine>
</BrowserRouter>
);
// Test workflow steps
await waitFor(() => {
expect(screen.getByText("Expected Result")).toBeInTheDocument();
});
});
});
E2E Test Template
import { test, expect } from "@playwright/test";
test.describe("Feature E2E", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/your-page");
});
test("should work as expected", async ({ page }) => {
await page.getByRole("button", { name: /action/i }).click();
await expect(page).toHaveURL(/expected-url/);
await expect(page.getByText("Success")).toBeVisible();
});
});
Best Practices
General
- Test User Behavior: Test what users see and do, not implementation details
- Accessible Queries: Use
getByRole,getByLabelTextovergetByTestId - Async Actions: Always
awaituser interactions - Isolation: Each test should be independent
- Cleanup: Use
beforeEachandafterEachfor setup/teardown
Unit Tests
- Mock external dependencies (API calls, contexts)
- Test one component at a time
- Cover edge cases (empty state, errors, loading)
- Test accessibility (ARIA attributes, keyboard navigation)
Integration Tests
- Test component interactions
- Use real providers when possible
- Mock only external APIs
- Test complete workflows
E2E Tests
- Test critical user journeys
- Use realistic test data
- Handle timing issues with
waitFor - Test across browsers
- Keep tests fast (avoid unnecessary waits)
Troubleshooting
Common Issues
"Cannot find module" errors
# Install dependencies
pnpm install
Tests timeout
// Increase timeout for slow tests
test("slow test", async () => {
// ...
}, 10000); // 10 seconds
"Element not found" errors
// Wait for element to appear
await waitFor(() => {
expect(screen.getByText("Hello")).toBeInTheDocument();
});
// Or use findBy (waits automatically)
const element = await screen.findByText("Hello");
E2E tests fail locally
# Ensure dev server is running
pnpm dev
# Or let Playwright start it automatically (configured in playwright.config.ts)
pnpm test:e2e
Browser not installed
# Install Playwright browsers
pnpm exec playwright install
Debug Mode
Vitest
# Run tests in debug mode
pnpm test --inspect-brk
# Open Chrome DevTools → chrome://inspect
Playwright
# Run with UI mode (recommended)
pnpm test:e2e:ui
# Run in headed mode
pnpm test:e2e:headed
# Debug specific test
pnpm exec playwright test --debug login.spec.ts
Coverage Reports
# Generate coverage report
pnpm test:coverage
# Open HTML report
open coverage/index.html
Coverage is configured in vitest.config.ts:
- Text output to terminal
- JSON for CI integration
- HTML for detailed viewing
CI/CD Integration
Tests run on every push and PR:
- Unit tests: Fast feedback (< 30s)
- E2E tests: Comprehensive validation (2-5 min)
- Coverage reports uploaded to CI
Test Statistics
Current test coverage:
- Unit Tests: 29 tests across 3 files
- Integration Tests: 10 tests across 2 files
- E2E Tests: 26 tests across 3 files
- Total: 65 tests
Coverage Metrics
- Components: 85%+ coverage
- Critical paths: 100% coverage
- Edge cases: Well tested
Next Steps
To improve test coverage:
- Add tests for remaining components (LocaleSwitcher, WorkflowActions, etc.)
- Increase integration test scenarios
- Add visual regression tests (Playwright + Snapshots)
- Performance testing (Lighthouse CI)
- Accessibility testing (axe-core integration)