> Meticulous records real user sessions from a web app and replays them against each commit to catch visual regressions.
>
> Full AI-readable docs index (complete project setup journey + every page as markdown): https://app.meticulous.ai/llms.txt

# React with Vite - Complete Setup Guide

Complete guide for setting up Meticulous with React applications built with Vite.

---

## Overview

Vite is a fast build tool for modern web applications. This guide covers:

- **Recorder installation** in `index.html`
- **CI/CD configuration** with static asset upload
- **Authentication handling**
- **Common patterns** and troubleshooting

**Prerequisites**:
- React application using Vite
- Basic familiarity with [Meticulous concepts](/docs/onboarding-guide)

---

## Quick Start

### Step 1: Install Recorder in index.html

Add the Meticulous recorder script to your `index.html` **before any other scripts**.

**File**: `index.html`

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Your App Name</title>

    <!-- Meticulous recorder - MUST be first script -->
    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
    <script
      data-project-id="YOUR_PROJECT_ID"
      src="https://snippet.meticulous.ai/v1/meticulous.js"
    ></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

**Important**: The recorder must load before your application code to capture all events.

---

### Step 2: Configure GitHub Actions Workflow

Vite builds to static files, so we use the `upload-assets` action instead of `cloud-compute`.

**File**: `.github/workflows/meticulous.yml`

```yaml
name: Meticulous Tests

on:
  push:
    branches: [main]
  pull_request: {}
  workflow_dispatch:
    inputs:
      meticulous-commit-sha:
        description: Commit Meticulous has asked this run to build. Defaults to the branch head.
        required: false

permissions:
  actions: write
  contents: read
  issues: write
  pull-requests: write
  statuses: read

env:
  # Prefer the dispatched commit when set; otherwise the PR head. On pull_request github.sha is the merge commit,
  # not the PR head SHA that Meticulous looks up.
  METICULOUS_COMMIT_SHA: ${{ github.event.inputs['meticulous-commit-sha'] || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ env.METICULOUS_COMMIT_SHA }}

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build app
        run: npm run build
        env:
          NODE_ENV: production

      - name: Upload and test
        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-directory: "dist"
          rewrites: |
            [
              { "source": "/(.*)", "destination": "/index.html" }
            ]
```

---

### Step 3: Add API Token Secret

1. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
2. Go to GitHub repo → Settings → Secrets and variables → Actions
3. Create secret named `METICULOUS_API_TOKEN` with your token

---

## How It Works

### upload-assets Action

The `upload-assets` action:
1. Uploads your built static files to Meticulous
2. Serves them on a temporary URL
3. Runs tests against that URL
4. Reports diffs back to your PR

**Key differences from cloud-compute**:
- Simpler setup (no server needed)
- Faster for static sites
- Can't test server-side logic
- No backend API calls (unless mocked)

### Rewrites Configuration

The `rewrites` parameter handles client-side routing:

```json
[
  { "source": "/(.*)", "destination": "/index.html" }
]
```

This ensures all routes (`/about`, `/dashboard`, etc.) serve `index.html`, allowing React Router to handle routing.

---

## Common Patterns

### Pattern 1: Detect Test Mode

Use `window.Meticulous.isRunningAsTest` to detect when running as a test:

```typescript
// In any component
function MyComponent() {
  const isTest = window.Meticulous?.isRunningAsTest

  if (isTest) {
    // Skip animations, use test data, etc.
  }

  return <div>...</div>
}
```

### Pattern 2: Bypass Authentication

```typescript
// In App.tsx or auth provider
import { useEffect } from 'react'

function App() {
  useEffect(() => {
    if (window.Meticulous?.isRunningAsTest) {
      // Mock authentication for tests
      localStorage.setItem('auth-token', 'test-token')
      localStorage.setItem('user', JSON.stringify({
        id: 'test-user',
        name: 'Test User',
        email: 'test@example.com'
      }))
    }
  }, [])

  return <YourApp />
}
```

### Pattern 3: Mock API Responses

Since there's no backend in upload-assets mode, API calls need to be mocked:

**Option 1: Use MSW (Mock Service Worker)**

```typescript
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

// src/main.tsx
if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
  const { worker } = await import('./mocks/browser')
  await worker.start()
}
```

**Option 2: Use recorded custom values**

```typescript
// During recording
window.Meticulous?.recordCustomValues?.({
  apiData: await fetchFromAPI()
})

// During replay
const data = window.Meticulous?.isRunningAsTest
  ? window.Meticulous.getCustomValues()?.apiData
  : await fetchFromAPI()
```

### Pattern 4: Handle Environment Variables

Vite exposes environment variables prefixed with `VITE_`:

```typescript
const apiUrl = import.meta.env.VITE_API_URL

// Use different URL for tests
const effectiveUrl = window.Meticulous?.isRunningAsTest
  ? 'https://api.test.example.com'
  : apiUrl
```

---

## Complete Example

### File Structure

```
your-app/
├── src/
│   ├── main.tsx              # Entry point
│   ├── App.tsx               # Main app component
│   ├── components/
│   ├── lib/
│   │   └── auth.ts           # Auth utilities
│   └── mocks/                # MSW mocks (optional)
├── index.html                # Recorder installation
├── vite.config.ts            # Vite configuration
├── .github/
│   └── workflows/
│       └── meticulous.yml    # CI/CD
└── package.json
```

### Example: Protected Route

**File**: `src/App.tsx`

```typescript
import { useEffect, useState } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'

function App() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Mock auth for tests
    if (window.Meticulous?.isRunningAsTest) {
      setUser({
        id: 'test-user-123',
        name: 'Test User',
        email: 'test@example.com'
      })
      setLoading(false)
      return
    }

    // Normal auth flow
    checkAuth().then(user => {
      setUser(user)
      setLoading(false)
    })
  }, [])

  if (loading) {
    return <div>Loading...</div>
  }

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route
          path="/dashboard"
          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
        />
        <Route path="/login" element={<LoginPage />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App
```

---

## CI/CD Configuration Details

### Environment Variables

Add build-time environment variables:

```yaml
- name: Build app
  run: npm run build
  env:
    VITE_API_URL: "https://api.example.com"
    VITE_APP_NAME: "My App"
    NODE_ENV: production
```

**In code**:
```typescript
const apiUrl = import.meta.env.VITE_API_URL
```

### Custom Build Directory

If Vite outputs to a different directory:

```yaml
- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "build" # Change from default "dist"
    rewrites: |
      [
        { "source": "/(.*)", "destination": "/index.html" }
      ]
```

### Multiple Rewrites

For complex routing:

```yaml
rewrites: |
  [
    { "source": "/api/(.*)", "destination": "/api/index.html" },
    { "source": "/(.*)", "destination": "/index.html" }
  ]
```

---

## Troubleshooting

### Issue: Recorder Not Loading

**Symptom**: `window.Meticulous` is undefined

**Checks**:
1. Verify recorder script is in `index.html` `<head>`
2. Check project ID is correct
3. Check for CSP blocking (console errors)
4. Verify script loads before `src/main.tsx`

**Fix**: Ensure correct order in `index.html`:

```html
<head>
  <!-- Recorder FIRST -->
  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>

  <!-- Then other scripts -->
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
```

---

### Issue: Routes Return 404

**Symptom**: Direct navigation to `/about` returns 404

**Cause**: Missing rewrite configuration

**Fix**: Add rewrites to workflow:

```yaml
rewrites: |
  [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
```

---

### Issue: API Calls Fail

**Symptom**: API requests fail during tests

**Cause**: No backend in upload-assets mode

**Solutions**:

**Option 1: Mock APIs with MSW** (recommended)

See Pattern 3 above. Keeps you on `upload-assets`, which is the simplest and most reliable workflow.

**Option 2: Switch to `upload-container`** (if you have a backend you want to actually run)

Build a Docker image that runs both your frontend and backend (or just your backend, if your built frontend is what `upload-assets` was uploading), and switch the workflow to `upload-container`:

```yaml
- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    tags: my-app:${{ env.METICULOUS_COMMIT_SHA }}
    platforms: linux/amd64
    push: false
    load: true

- name: Run Meticulous tests
  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    image-tag: my-app:${{ env.METICULOUS_COMMIT_SHA }}
    container-port: 5173
```

---

### Issue: Build Fails

**Symptom**: `npm run build` fails in CI

**Common causes**:
1. TypeScript errors
2. Missing environment variables
3. Linting errors treated as build errors

**Debug**:

```yaml
- name: Build app
  run: npm run build
  env:
    CI: false # Treats warnings as non-blocking
    NODE_ENV: production
```

---

### Issue: False Positive Diffs

**Symptom**: Tests show diffs for content that hasn't changed

**Common causes**:
1. Animations not completing
2. Random IDs or keys
3. Timestamps

**Fixes**:

**Animations**: Disable in tests
```typescript
const duration = window.Meticulous?.isRunningAsTest ? 0 : 300
```

**Random IDs**: Use deterministic values
```typescript
const generateId = () => {
  if (window.Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return crypto.randomUUID()
}
```

**Timestamps**: Add `meticulous-ignore` class
```typescript
<span className="meticulous-ignore">
  {new Date().toLocaleString()}
</span>
```

Learn more: [Fix False Positive Diffs](/docs/how-to/fix-false-positive-diffs)

---

## Testing Best Practices

### 1. Test Locally First

Run tests locally before CI:

```bash
# Build your app
npm run build

# Serve built files
npx serve dist

# In another terminal, run Meticulous
npx @alwaysmeticulous/cli simulate \
  --sessionId="YOUR_SESSION_ID" \
  --appUrl="http://localhost:3000"
```

### 2. Handle Loading States

Ensure loading states complete:

```typescript
useEffect(() => {
  const fetchData = async () => {
    setLoading(true)
    const data = await getData()
    setData(data)
    setLoading(false)
  }

  fetchData()
}, [])

if (loading) {
  return <div>Loading...</div>
}
```

### 3. Use Skeleton Screens

Instead of spinners:

```typescript
if (loading) {
  return <SkeletonCard /> // Consistent placeholder
}
```

---

## Advanced Configuration

### Monorepo Setup

If your Vite app is in a subdirectory:

```yaml
- name: Install dependencies
  working-directory: ./apps/frontend
  run: npm ci

- name: Build app
  working-directory: ./apps/frontend
  run: npm run build

- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "./apps/frontend/dist"
    rewrites: |
      [
        { "source": "/(.*)", "destination": "/index.html" }
      ]
```

### Custom Vite Config

If you have a custom Vite config:

**File**: `vite.config.ts`

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
  server: {
    port: 5173,
  }
})
```

---

## See Also

- [Onboarding Guide](/docs/onboarding-guide) - General Meticulous setup
- [Troubleshoot Authentication](/docs/how-to/troubleshoot-auth) - Auth patterns and solutions
- [Fix False Positives](/docs/how-to/fix-false-positive-diffs) - Handle non-deterministic content
