Complete guide for setting up Meticulous with React applications built with Vite.
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
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.
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" }
]
- 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
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)
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.
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>
}
// 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 />
}
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()
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
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
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
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
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" }
]
For complex routing:
rewrites: |
[
{ "source": "/api/(.*)", "destination": "/api/index.html" },
{ "source": "/(.*)", "destination": "/index.html" }
]
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>
Symptom: Direct navigation to /about returns 404
Cause: Missing rewrite configuration
Fix: Add rewrites to workflow:
rewrites: |
[
{ "source": "/(.*)", "destination": "/index.html" }
]
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.
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
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
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"
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>
}
Instead of spinners:
if (loading) {
return <SkeletonCard /> // Consistent placeholder
}
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" }
]
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,
}
})
- Onboarding Guide - General Meticulous setup
- Troubleshoot Authentication - Auth patterns and solutions
- Fix False Positives - Handle non-deterministic content