Skip to main content

Frontend Testing Guide

Complete testing documentation for the Framework M frontend application.

Table of Contents

Overview

The frontend uses a comprehensive testing strategy with three layers:

  1. Unit Tests - Test individual components in isolation
  2. Integration Tests - Test component interactions and workflows
  3. E2E Tests - Test complete user workflows across browsers

Technology Stack

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-dom matchers
  • Mocks window.matchMedia (for theme detection)
  • Mocks localStorage and sessionStorage
  • Mocks IntersectionObserver and ResizeObserver
  • 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

  1. Test User Behavior: Test what users see and do, not implementation details
  2. Accessible Queries: Use getByRole, getByLabelText over getByTestId
  3. Async Actions: Always await user interactions
  4. Isolation: Each test should be independent
  5. Cleanup: Use beforeEach and afterEach for 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:

  1. Add tests for remaining components (LocaleSwitcher, WorkflowActions, etc.)
  2. Increase integration test scenarios
  3. Add visual regression tests (Playwright + Snapshots)
  4. Performance testing (Lighthouse CI)
  5. Accessibility testing (axe-core integration)

References