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
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
# Important: The workflow needs to run both on pushes to your main branch and on
# pull requests. It needs to run on your main branch because it'll use the results
# from the base commit of the PR on the main branch to compare against.
on:
push:
branches:
- main
pull_request: {}
# Important: We need the workflow to be triggered on workflow_dispatch events,
# so that Meticulous can run the workflow on the base commit to compare
# against if an existing workflow hasn't run. The meticulous-commit-sha input
# lets Meticulous ask for a specific commit (e.g. stacked PRs); without it,
# a dispatched run can only build whatever the branch currently points at.
workflow_dispatch:
inputs:
meticulous-commit-sha:
description: Commit Meticulous has asked this run to build. Defaults to the branch head.
required: false
# Important: The workflow needs all the permissions below.
# These permissions are mainly needed to post and update the status check and
# feedback comment on your PR. Meticulous won't work without them.
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:
name: Meticulous
runs-on: ubuntu-latest
steps:
# Same workflow file as the upload step — ensure-base dispatches *this*
# workflow on the base branch. Run it before checkout/build so the base
# can start while this job continues. Needs no checkout. Pass the same
# ref as checkout so we pre-warm the base the upload step will ask for.
- name: Ensure base tests exist
uses: alwaysmeticulous/report-diffs-action/ensure-base@v1
with:
api-token: ${{ secrets.METICULOUS_API_TOKEN }}
ref: ${{ env.METICULOUS_COMMIT_SHA }}
- name: Checkout repository
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
- Get your API token from Meticulous dashboard → Project Settings
- Go to GitHub repo → Settings → Secrets and variables → Actions
- Create secret named
METICULOUS_API_TOKENwith your token
How It Works
upload-assets Action
The upload-assets action:
- Uploads your built static files to Meticulous
- Serves them on a temporary URL
- Runs tests against that URL
- 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:
- Verify recorder script is in
index.html<head> - Check project ID is correct
- Check for CSP blocking (console errors)
- 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: 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:
- 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:
- TypeScript errors
- Missing environment variables
- 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:
- Animations not completing
- Random IDs or keys
- 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'
import CssSourcemapPlugin from '@alwaysmeticulous/recorder-plugin/css-sourcemap'
export default defineConfig({
plugins: [react(), CssSourcemapPlugin()],
build: {
outDir: 'dist',
sourcemap: true,
},
server: {
port: 5173,
}
})
Important: build.sourcemap covers JavaScript only — it does nothing for CSS. Vite emits no CSS source maps for production builds at all, so without extra help Meticulous cannot attribute stylesheet coverage back to the files in your repo. @alwaysmeticulous/recorder-plugin/css-sourcemap emits a .css.map for each CSS asset to close that gap.
The plugin disables Vite's CSS minification, which is what makes the maps accurate, so enable it on the build whose coverage Meticulous collects rather than on every production build. See Viewing source coverage information for the accuracy details and the full set of options.
See Also
- Onboarding Guide - General Meticulous setup
- Troubleshoot Authentication - Auth patterns and solutions
- Fix False Positives - Handle non-deterministic content