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:


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

<!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

name: Meticulous Tests

on:
  push:
    branches: [main]
  pull_request: {}
  workflow_dispatch: {}

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

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

    steps:
      - uses: actions/checkout@v4

      - 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 → 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:

[
  { "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:

// 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

// 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)

// 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

// 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_:

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

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:

- 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:

const apiUrl = import.meta.env.VITE_API_URL

Custom Build Directory

If Vite outputs to a different directory:

- 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:

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:

<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:

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: Switch to cloud-compute (if you have a backend)

- name: Start backend
  run: npm run start:api &

- name: Start frontend
  run: npm run dev &

- name: Run Meticulous tests
  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-url: "http://localhost:5173"

Option 2: Mock APIs with MSW

See Pattern 3 above.


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:

- 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

const duration = window.Meticulous?.isRunningAsTest ? 0 : 300

Random IDs: Use deterministic values

const generateId = () => {
  if (window.Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return crypto.randomUUID()
}

Timestamps: Add meticulous-ignore class

<span className="meticulous-ignore">
  {new Date().toLocaleString()}
</span>

Learn more: Fix False Positive Diffs


Testing Best Practices

1. Test Locally First

Run tests locally before CI:

# 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:

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:

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

Advanced Configuration

Monorepo Setup

If your Vite app is in a subdirectory:

- 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

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