Complete guide for setting up Meticulous with Vue 3 applications built with Vite.
Vue 3 with Vite provides a fast development experience. This guide covers:
- Recorder installation in
index.html - CI/CD configuration with static asset upload
- Authentication handling
- Common patterns and troubleshooting
Prerequisites:
- Vue 3 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" href="/favicon.ico" />
<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="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Important: The recorder must load before your application code.
Vue/Vite builds to static files, so we use the upload-assets action.
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 files and runs tests against them.
Build output: Vite builds to dist/ directory by default.
Handles client-side routing (Vue Router):
[
{ "source": "/(.*)", "destination": "/index.html" }
]
Use window.Meticulous.isRunningAsTest in your components:
Options API:
<template>
<div>
<p v-if="isTest">Running as test</p>
<p v-else>Running normally</p>
</div>
</template>
<script>
export default {
data() {
return {
isTest: window.Meticulous?.isRunningAsTest || false
}
}
}
</script>
Composition API:
<template>
<div>
<p v-if="isTest">Running as test</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isTest = ref(window.Meticulous?.isRunningAsTest || false)
</script>
File: src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// Mock authentication for tests
if (window.Meticulous?.isRunningAsTest) {
localStorage.setItem('auth-token', 'test-token')
localStorage.setItem('user', JSON.stringify({
id: 'test-user',
name: 'Test User',
email: 'test@example.com'
}))
}
app.use(createPinia())
app.use(router)
app.mount('#app')
Handle authentication in router:
File: src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocationNormalized } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue')
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to: RouteLocationNormalized) => {
// Skip auth check during tests
if (window.Meticulous?.isRunningAsTest) {
return true
}
// Normal auth check
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'login', query: { redirect: to.fullPath } }
}
return true
})
export default router
Create a reusable composable:
File: src/composables/useMeticulous.ts
import { ref, readonly } from 'vue'
export function useMeticulous() {
const isRunningAsTest = ref(
window.Meticulous?.isRunningAsTest || false
)
const recordCustomValues = (values: Record<string, any>) => {
window.Meticulous?.recordCustomValues?.(values)
}
const getCustomValues = () => {
return window.Meticulous?.getCustomValues?.()
}
return {
isRunningAsTest: readonly(isRunningAsTest),
recordCustomValues,
getCustomValues
}
}
Usage:
<script setup>
import { useMeticulous } from '@/composables/useMeticulous'
const { isRunningAsTest } = useMeticulous()
</script>
your-app/
├── src/
│ ├── main.ts # Entry point
│ ├── App.vue # Root component
│ ├── router/
│ │ └── index.ts # Router configuration
│ ├── stores/ # Pinia stores
│ ├── views/ # Page components
│ ├── components/ # Reusable components
│ └── composables/
│ └── useMeticulous.ts # Test detection composable
├── index.html # Recorder installation
├── vite.config.ts # Vite configuration
├── .github/
│ └── workflows/
│ └── meticulous.yml # CI/CD
└── package.json
File: src/views/DashboardView.vue
<template>
<div class="dashboard">
<h1>Welcome, {{ user?.name }}!</h1>
<p>Email: {{ user?.email }}</p>
<div v-if="loading">
<p>Loading dashboard...</p>
</div>
<div v-else-if="error">
<p class="error">{{ error }}</p>
</div>
<div v-else>
<!-- Dashboard content -->
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
interface User {
id: string
name: string
email: string
}
const router = useRouter()
const user = ref<User | null>(null)
const loading = ref(true)
const error = ref('')
onMounted(async () => {
// Mock data for tests
if (window.Meticulous?.isRunningAsTest) {
user.value = {
id: 'test-user-123',
name: 'Test User',
email: 'test@example.com'
}
loading.value = false
return
}
// Normal data fetching
try {
const response = await fetch('/api/user')
if (!response.ok) throw new Error('Failed to fetch user')
user.value = await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
router.push('/login')
} finally {
loading.value = false
}
})
</script>
Vite exposes environment variables prefixed with VITE_:
- 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
Ensure TypeScript recognizes Vite env variables:
File: src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_NAME: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Symptom: window.Meticulous is undefined
Checks:
- Verify recorder script in
index.html<head> - Check project ID is correct
- Check for CSP blocking
Fix: Ensure correct placement:
<head>
<!-- Recorder FIRST -->
<script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>
<!-- Then other elements -->
<title>Your App</title>
</head>
Symptom: Direct navigation to routes returns 404
Cause: Missing rewrite configuration
Fix:
rewrites: |
[
{ "source": "/(.*)", "destination": "/index.html" }
]
Symptom: Property 'Meticulous' does not exist on type 'Window'.
Fix: Add type declarations
File: src/types/meticulous.d.ts
interface Meticulous {
isRunningAsTest?: boolean
recordCustomValues?: (values: Record<string, any>) => void
getCustomValues?: () => Record<string, any> | undefined
pause?: () => void
resume?: () => void
}
declare global {
interface Window {
Meticulous?: Meticulous
}
}
export {}
Common causes:
- Animations not completing
- Random data
- Timestamps
Fixes:
Disable animations in tests:
<template>
<Transition :duration="transitionDuration">
<div>Content</div>
</Transition>
</template>
<script setup>
import { computed } from 'vue'
const transitionDuration = computed(() =>
window.Meticulous?.isRunningAsTest ? 0 : 300
)
</script>
Deterministic IDs:
const generateId = () => {
if (window.Meticulous?.isRunningAsTest) {
return 'test-id-12345'
}
return crypto.randomUUID()
}
Ignore timestamps:
<template>
<span class="meticulous-ignore">
{{ new Date().toLocaleString() }}
</span>
</template>
Learn more: Fix False Positive Diffs
Always show loading states:
<template>
<div v-if="loading">
<SkeletonLoader />
</div>
<div v-else-if="error">
<ErrorMessage :error="error" />
</div>
<div v-else>
<!-- Content -->
</div>
</template>
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
# Build your app
npm run build
# Serve built files
npx serve dist
# Verify at http://localhost:3000
- 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" }
]
File: vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: 'dist',
sourcemap: true
}
})
- Onboarding Guide - General Meticulous setup
- Troubleshoot Authentication - Auth patterns and solutions
- Fix False Positives - Handle non-deterministic content