Vue 3 with Vite - Complete Setup Guide

Complete guide for setting up Meticulous with Vue 3 applications built with Vite.


Overview

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:


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" 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.


Step 2: Configure GitHub Actions Workflow

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" }
            ]

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 uploads your built files and runs tests against them.

Build output: Vite builds to dist/ directory by default.

Rewrites Configuration

Handles client-side routing (Vue Router):

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

Common Patterns

Pattern 1: Detect Test Mode

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>

Pattern 2: Bypass Authentication

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

Pattern 3: Router Navigation Guards

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

Pattern 4: Composable for Test Detection

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>

Complete Example

File Structure

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

Example: Protected View

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>

CI/CD Configuration Details

Environment Variables

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

TypeScript Configuration

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
}

Troubleshooting

Issue: Recorder Not Loading

Symptom: window.Meticulous is undefined

Checks:

  1. Verify recorder script in index.html <head>
  2. Check project ID is correct
  3. 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>

Issue: Routes Return 404

Symptom: Direct navigation to routes returns 404

Cause: Missing rewrite configuration

Fix:

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

Issue: TypeScript Errors

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 {}

Issue: False Positive Diffs

Common causes:

  1. Animations not completing
  2. Random data
  3. 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


Testing Best Practices

1. Handle Loading States

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>

2. Use Suspense for Async Components

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

3. Test Locally First

# Build your app
npm run build

# Serve built files
npx serve dist

# Verify at http://localhost:3000

Advanced Configuration

Monorepo Setup

- 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

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

See Also