Reference

Reference Untitled

Untitled

OtherEvergreenPublic

Advanced Nuxt 3 Testing Examples & Migration Guide

Callout Component with Nuxt 3 Patterns

File: components/Callout.vue

<template>
  <div
    :class="calloutClasses"
    :role="semanticRole"
    :aria-live="ariaLive"
    :data-testid="testId"
  >
    <component
      v-if="iconComponent"
      :is="iconComponent"
      class="callout-icon"
      :size="20"
    />

    <div class="callout-content">
      <h4 v-if="title" class="callout-title">
        {{ title }}
      </h4>
      <div class="callout-message">
        <slot />
      </div>

      <div v-if="$slots.actions" class="callout-actions mt-3">
        <slot name="actions" />
      </div>
    </div>

    <button
      v-if="dismissible"
      class="callout-dismiss"
      type="button"
      :aria-label="dismissLabel"
      @click="handleDismiss"
    >
      <PhX :size="16" />
    </button>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import {
  PhInfo,
  PhCheckCircle,
  PhWarning,
  PhXCircle,
  PhX
} from '@phosphor-icons/vue'

export interface CalloutProps {
  type?: 'info' | 'success' | 'warning' | 'error'
  title?: string
  dismissible?: boolean
  dismissLabel?: string
  testId?: string
}

interface CalloutEmits {
  dismiss: []
}

const props = withDefaults(defineProps<CalloutProps>(), {
  type: 'info',
  dismissLabel: 'Dismiss notification',
})

const emit = defineEmits<CalloutEmits>()

// Computed properties using Vue 3 reactivity
const calloutClasses = computed(() => [
  'callout',
  `callout-${props.type}`
])

const iconComponent = computed(() => {
  const iconMap = {
    info: PhInfo,
    success: PhCheckCircle,
    warning: PhWarning,
    error: PhXCircle
  }
  return iconMap[props.type]
})

const semanticRole = computed(() => {
  return props.type === 'error' ? 'alert' : 'status'
})

const ariaLive = computed(() => {
  return props.type === 'error' ? 'assertive' : 'polite'
})

// Event handlers
const handleDismiss = () => {
  emit('dismiss')
}

// Nuxt composables for enhanced functionality
const { $router } = useNuxtApp()

// Example: Track callout interactions
const trackCallout = (action: string) => {
  // You could use Nuxt analytics modules here
  console.log(`Callout ${props.type} ${action}`)
}

// Lifecycle for tracking
onMounted(() => {
  trackCallout('viewed')
})
</script>

<style scoped>
.callout {
  @apply rounded-lg border p-4 flex gap-3 relative;
}

.callout-icon {
  @apply flex-shrink-0 mt-0.5;
}

.callout-content {
  @apply flex-1 min-w-0;
}

.callout-title {
  @apply font-semibold text-sm mb-1;
}

.callout-message {
  @apply text-sm leading-relaxed;
}

.callout-dismiss {
  @apply absolute top-2 right-2 p-1 rounded hover:bg-black/10
         transition-colors duration-200 focus:outline-none
         focus:ring-2 focus:ring-offset-1;
}

.callout-actions {
  @apply flex gap-2;
}

/* Variant styles */
.callout-info {
  @apply bg-info/10 border-info/20 text-info-900;
}

.callout-info .callout-icon,
.callout-info .callout-dismiss {
  @apply text-info focus:ring-info/50;
}

.callout-success {
  @apply bg-success/10 border-success/20 text-success-900;
}

.callout-success .callout-icon,
.callout-success .callout-dismiss {
  @apply text-success focus:ring-success/50;
}

.callout-warning {
  @apply bg-warning/10 border-warning/20 text-warning-900;
}

.callout-warning .callout-icon,
.callout-warning .callout-dismiss {
  @apply text-warning focus:ring-warning/50;
}

.callout-error {
  @apply bg-error/10 border-error/20 text-error-900;
}

.callout-error .callout-icon,
.callout-error .callout-dismiss {
  @apply text-error focus:ring-error/50;
}

/* Dark mode support */
.dark .callout-info {
  @apply bg-info/20 border-info/30 text-info-300;
}

.dark .callout-success {
  @apply bg-success/20 border-success/30 text-success-300;
}

.dark .callout-warning {
  @apply bg-warning/20 border-warning/30 text-warning-300;
}

.dark .callout-error {
  @apply bg-error/20 border-error/30 text-error-300;
}
</style>

Advanced Callout Tests

File: components/Callout.test.ts

import { describe, it, expect, vi } from 'vitest'
import { nextTick } from 'vue'
import { axe } from 'vitest-axe'
import { mountWithNuxt } from '../tests/utils/nuxt-test-utils'
import Callout from './Callout.vue'
import Button from './Button.vue'

describe('Callout Component (Nuxt 3)', () => {
  describe('Rendering & Props', () => {
    it('renders with default props', () => {
      const wrapper = mountWithNuxt(Callout, {
        slots: { default: 'Default callout message' }
      })

      expect(wrapper.find('.callout').exists()).toBe(true)
      expect(wrapper.text()).toContain('Default callout message')
      expect(wrapper.classes()).toContain('callout-info') // default type
    })

    it('renders all callout types correctly', () => {
      const types = ['info', 'success', 'warning', 'error'] as const

      types.forEach(type => {
        const wrapper = mountWithNuxt(Callout, {
          props: { type },
          slots: { default: `${type} message` }
        })

        expect(wrapper.classes()).toContain(`callout-${type}`)
        expect(wrapper.find('.callout-icon').exists()).toBe(true)
      })
    })

    it('renders with title', () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { title: 'Important Notice' },
        slots: { default: 'Message content' }
      })

      expect(wrapper.find('.callout-title').text()).toBe('Important Notice')
    })

    it('renders dismiss button when dismissible', () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { dismissible: true },
        slots: { default: 'Dismissible message' }
      })

      expect(wrapper.find('.callout-dismiss').exists()).toBe(true)
    })
  })

  describe('Vue 3 Reactivity', () => {
    it('updates classes when type changes', async () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { type: 'info' },
        slots: { default: 'Test message' }
      })

      expect(wrapper.classes()).toContain('callout-info')

      await wrapper.setProps({ type: 'error' })
      expect(wrapper.classes()).toContain('callout-error')
      expect(wrapper.classes()).not.toContain('callout-info')
    })

    it('updates icon when type changes', async () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { type: 'info' },
        slots: { default: 'Test message' }
      })

      // Icon should be present
      expect(wrapper.find('.callout-icon').exists()).toBe(true)

      await wrapper.setProps({ type: 'success' })
      // Icon should still be present but different component
      expect(wrapper.find('.callout-icon').exists()).toBe(true)
    })

    it('reactively shows/hides dismiss button', async () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { dismissible: false },
        slots: { default: 'Test message' }
      })

      expect(wrapper.find('.callout-dismiss').exists()).toBe(false)

      await wrapper.setProps({ dismissible: true })
      expect(wrapper.find('.callout-dismiss').exists()).toBe(true)
    })
  })

  describe('Events & Interactions', () => {
    it('emits dismiss event when dismiss button clicked', async () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { dismissible: true },
        slots: { default: 'Dismissible message' }
      })

      await wrapper.find('.callout-dismiss').trigger('click')
      expect(wrapper.emitted().dismiss).toBeTruthy()
      expect(wrapper.emitted().dismiss).toHaveLength(1)
    })

    it('does not emit dismiss when not dismissible', () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { dismissible: false },
        slots: { default: 'Non-dismissible message' }
      })

      expect(wrapper.find('.callout-dismiss').exists()).toBe(false)
      expect(wrapper.emitted().dismiss).toBeFalsy()
    })
  })

  describe('Slots & Composition', () => {
    it('renders default slot content', () => {
      const wrapper = mountWithNuxt(Callout, {
        slots: {
          default: '<p>Rich <strong>HTML</strong> content</p>'
        }
      })

      expect(wrapper.html()).toContain('<p>Rich <strong>HTML</strong> content</p>')
    })

    it('renders actions slot', () => {
      const wrapper = mountWithNuxt(Callout, {
        slots: {
          default: 'Message with actions',
          actions: '<button class="test-action">Take Action</button>'
        }
      })

      expect(wrapper.find('.callout-actions').exists()).toBe(true)
      expect(wrapper.find('.test-action').text()).toBe('Take Action')
    })

    it('integrates with Button component', () => {
      const wrapper = mountWithNuxt(Callout, {
        slots: {
          default: 'Message with button',
          actions: '<Button variant="primary">Action Button</Button>'
        },
        global: {
          components: { Button }
        }
      })

      expect(wrapper.findComponent(Button).exists()).toBe(true)
    })
  })

  describe('Accessibility (ARIA)', () => {
    it('has correct ARIA role for different types', () => {
      // Error callouts should have alert role
      const errorWrapper = mountWithNuxt(Callout, {
        props: { type: 'error' },
        slots: { default: 'Error message' }
      })
      expect(errorWrapper.attributes('role')).toBe('alert')
      expect(errorWrapper.attributes('aria-live')).toBe('assertive')

      // Other types should have status role
      const infoWrapper = mountWithNuxt(Callout, {
        props: { type: 'info' },
        slots: { default: 'Info message' }
      })
      expect(infoWrapper.attributes('role')).toBe('status')
      expect(infoWrapper.attributes('aria-live')).toBe('polite')
    })

    it('has accessible dismiss button', () => {
      const wrapper = mountWithNuxt(Callout, {
        props: {
          dismissible: true,
          dismissLabel: 'Close notification'
        },
        slots: { default: 'Dismissible message' }
      })

      const dismissButton = wrapper.find('.callout-dismiss')
      expect(dismissButton.attributes('aria-label')).toBe('Close notification')
      expect(dismissButton.attributes('type')).toBe('button')
    })

    it('passes accessibility tests', async () => {
      const wrapper = mountWithNuxt(Callout, {
        props: {
          type: 'warning',
          title: 'Warning Title',
          dismissible: true
        },
        slots: {
          default: 'Warning message content',
          actions: '<button>Take Action</button>'
        }
      })

      const results = await axe(wrapper.element)
      expect(results).toHaveNoViolations()
    })
  })

  describe('Nuxt Integration', () => {
    it('works with Nuxt composables', () => {
      const wrapper = mountWithNuxt(Callout, {
        slots: { default: 'Test message' }
      })

      // Component should mount without errors when using Nuxt composables
      expect(wrapper.exists()).toBe(true)
    })

    it('integrates with Nuxt routing', async () => {
      const mockPush = vi.fn()
      const wrapper = mountWithNuxt(Callout, {
        slots: {
          default: 'Navigation callout',
          actions: '<button @click="$router.push(\'/test\')">Navigate</button>'
        },
        global: {
          mocks: {
            $router: { push: mockPush }
          }
        }
      })

      // This would test navigation integration if you had it
      expect(wrapper.exists()).toBe(true)
    })
  })

  describe('Performance', () => {
    it('renders efficiently with computed properties', () => {
      const startTime = performance.now()

      const wrapper = mountWithNuxt(Callout, {
        props: { type: 'success', title: 'Success!' },
        slots: { default: 'Performance test' }
      })

      const endTime = performance.now()
      const renderTime = endTime - startTime

      expect(renderTime).toBeLessThan(16) // 60fps
      expect(wrapper.exists()).toBe(true)
    })

    it('handles reactive updates efficiently', async () => {
      const wrapper = mountWithNuxt(Callout, {
        props: { type: 'info' },
        slots: { default: 'Reactive test' }
      })

      const startTime = performance.now()

      // Multiple rapid updates
      await wrapper.setProps({ type: 'success' })
      await wrapper.setProps({ type: 'warning' })
      await wrapper.setProps({ type: 'error' })
      await wrapper.setProps({ dismissible: true })

      const endTime = performance.now()
      const updateTime = endTime - startTime

      expect(updateTime).toBeLessThan(10)
    })
  })
})

Advanced Composable with Testing

File: composables/useNotifications.ts

import { ref, computed, nextTick } from 'vue'

export interface Notification {
  id: string
  type: 'info' | 'success' | 'warning' | 'error'
  title?: string
  message: string
  duration?: number
  dismissible?: boolean
}

export const useNotifications = () => {
  const notifications = ref<Notification[]>([])
  const maxNotifications = ref(5)

  const activeNotifications = computed(() =>
    notifications.value.slice(0, maxNotifications.value)
  )

  const notificationCount = computed(() =>
    notifications.value.length
  )

  const add = (notification: Omit<Notification, 'id'>) => {
    const id = Date.now().toString() + Math.random().toString(36).substr(2, 9)
    const newNotification: Notification = {
      id,
      dismissible: true,
      duration: 5000,
      ...notification
    }

    notifications.value.unshift(newNotification)

    // Auto-dismiss if duration is set
    if (newNotification.duration && newNotification.duration > 0) {
      setTimeout(() => {
        remove(id)
      }, newNotification.duration)
    }

    return id
  }

  const remove = (id: string) => {
    const index = notifications.value.findIndex(n => n.id === id)
    if (index > -1) {
      notifications.value.splice(index, 1)
    }
  }

  const clear = () => {
    notifications.value = []
  }

  const success = (message: string, options?: Partial<Notification>) => {
    return add({ ...options, type: 'success', message })
  }

  const error = (message: string, options?: Partial<Notification>) => {
    return add({ ...options, type: 'error', message })
  }

  const warning = (message: string, options?: Partial<Notification>) => {
    return add({ ...options, type: 'warning', message })
  }

  const info = (message: string, options?: Partial<Notification>) => {
    return add({ ...options, type: 'info', message })
  }

  return {
    notifications: readonly(notifications),
    activeNotifications,
    notificationCount,
    add,
    remove,
    clear,
    success,
    error,
    warning,
    info
  }
}

// Global instance for app-wide notifications
let globalNotifications: ReturnType<typeof useNotifications> | null = null

export const useGlobalNotifications = () => {
  if (!globalNotifications) {
    globalNotifications = useNotifications()
  }
  return globalNotifications
}

File: composables/useNotifications.test.ts

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { useNotifications, useGlobalNotifications } from './useNotifications'

describe('useNotifications Composable', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  describe('Basic Functionality', () => {
    it('initializes with empty notifications', () => {
      const { notifications, notificationCount } = useNotifications()

      expect(notifications.value).toEqual([])
      expect(notificationCount.value).toBe(0)
    })

    it('adds notifications correctly', () => {
      const { add, notifications, notificationCount } = useNotifications()

      const id = add({
        type: 'info',
        message: 'Test notification'
      })

      expect(notifications.value).toHaveLength(1)
      expect(notificationCount.value).toBe(1)
      expect(notifications.value[0]).toMatchObject({
        id,
        type: 'info',
        message: 'Test notification',
        dismissible: true,
        duration: 5000
      })
    })

    it('removes notifications by id', () => {
      const { add, remove, notifications } = useNotifications()

      const id1 = add({ type: 'info', message: 'First' })
      const id2 = add({ type: 'success', message: 'Second' })

      expect(notifications.value).toHaveLength(2)

      remove(id1)
      expect(notifications.value).toHaveLength(1)
      expect(notifications.value[0].message).toBe('Second')
    })

    it('clears all notifications', () => {
      const { add, clear, notifications } = useNotifications()

      add({ type: 'info', message: 'First' })
      add({ type: 'success', message: 'Second' })

      expect(notifications.value).toHaveLength(2)

      clear()
      expect(notifications.value).toHaveLength(0)
    })
  })

  describe('Convenience Methods', () => {
    it('success method creates success notification', () => {
      const { success, notifications } = useNotifications()

      success('Operation completed!')

      expect(notifications.value[0]).toMatchObject({
        type: 'success',
        message: 'Operation completed!'
      })
    })

    it('error method creates error notification', () => {
      const { error, notifications } = useNotifications()

      error('Something went wrong!', { title: 'Error' })

      expect(notifications.value[0]).toMatchObject({
        type: 'error',
        message: 'Something went wrong!',
        title: 'Error'
      })
    })

    it('warning method creates warning notification', () => {
      const { warning, notifications } = useNotifications()

      warning('Please be careful')

      expect(notifications.value[0]).toMatchObject({
        type: 'warning',
        message: 'Please be careful'
      })
    })

    it('info method creates info notification', () => {
      const { info, notifications } = useNotifications()

      info('Here is some information')

      expect(notifications.value[0]).toMatchObject({
        type: 'info',
        message: 'Here is some information'
      })
    })
  })

  describe('Auto-dismiss Functionality', () => {
    it('auto-dismisses notifications after duration', async () => {
      const { add, notifications } = useNotifications()

      add({
        type: 'info',
        message: 'Auto dismiss test',
        duration: 1000
      })

      expect(notifications.value).toHaveLength(1)

      // Fast-forward time
      vi.advanceTimersByTime(1000)

      // Wait for next tick to allow removal
      await nextTick()

      expect(notifications.value).toHaveLength(0)
    })

    it('does not auto-dismiss when duration is 0', async () => {
      const { add, notifications } = useNotifications()

      add({
        type: 'info',
        message: 'Persistent notification',
        duration: 0
      })

      expect(notifications.value).toHaveLength(1)

      vi.advanceTimersByTime(10000)
      await nextTick()

      expect(notifications.value).toHaveLength(1)
    })
  })

  describe('Computed Properties', () => {
    it('activeNotifications respects max limit', () => {
      const { add, activeNotifications } = useNotifications()

      // Add more than max (5) notifications
      for (let i = 0; i < 7; i++) {
        add({ type: 'info', message: `Notification ${i}` })
      }

      expect(activeNotifications.value).toHaveLength(5)
    })

    it('notificationCount reflects total count', () => {
      const { add, notificationCount } = useNotifications()

      for (let i = 0; i < 7; i++) {
        add({ type: 'info', message: `Notification ${i}` })
      }

      expect(notificationCount.value).toBe(7)
    })
  })

  describe('Reactivity', () => {
    it('maintains reactivity when adding/removing', async () => {
      const { add, remove, notifications } = useNotifications()

      const id = add({ type: 'info', message: 'Test' })
      await nextTick()

      expect(notifications.value).toHaveLength(1)

      remove(id)
      await nextTick()

      expect(notifications.value).toHaveLength(0)
    })

    it('computed properties update reactively', async () => {
      const { add, activeNotifications, notificationCount } = useNotifications()

      expect(activeNotifications.value).toHaveLength(0)
      expect(notificationCount.value).toBe(0)

      add({ type: 'info', message: 'Test' })
      await nextTick()

      expect(activeNotifications.value).toHaveLength(1)
      expect(notificationCount.value).toBe(1)
    })
  })

  describe('Global Notifications', () => {
    it('returns same instance across calls', () => {
      const instance1 = useGlobalNotifications()
      const instance2 = useGlobalNotifications()

      expect(instance1).toBe(instance2)
    })

    it('maintains state across component instances', () => {
      const global1 = useGlobalNotifications()
      global1.add({ type: 'info', message: 'Global test' })

      const global2 = useGlobalNotifications()
      expect(global2.notifications.value).toHaveLength(1)
      expect(global2.notifications.value[0].message).toBe('Global test')
    })
  })

  describe('Performance', () => {
    it('handles large numbers of notifications efficiently', () => {
      const { add, notificationCount } = useNotifications()

      const startTime = performance.now()

      // Add many notifications
      for (let i = 0; i < 1000; i++) {
        add({ type: 'info', message: `Notification ${i}` })
      }

      const endTime = performance.now()
      const duration = endTime - startTime

      expect(notificationCount.value).toBe(1000)
      expect(duration).toBeLessThan(100) // Should be fast
    })
  })
})

Nuxt Page Testing Example

File: pages/design-system.vue

<template>
  <div class="design-system-page">
    <Head>
      <Title>Design System - {{ runtimeConfig.public.siteName }}</Title>
      <Meta name="description" :content="pageDescription" />
    </Head>

    <div class="container-fixed py-12">
      <header class="text-center mb-12">
        <h1 class="text-display-lg text-gray-900 mb-4">
          Design System
        </h1>
        <p class="text-body-lg text-gray-600 max-w-2xl mx-auto">
          {{ pageDescription }}
        </p>
      </header>

      <section class="mb-16">
        <h2 class="text-heading-xl text-gray-900 mb-8">Components</h2>

        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          <div
            v-for="component in components"
            :key="component.name"
            class="bg-white rounded-card p-6 border shadow-card hover:shadow-card-elevated transition-shadow"
          >
            <div class="flex items-start justify-between mb-4">
              <div class="flex items-center gap-3">
                <component :is="component.icon" class="text-2xl text-primary" />
                <h3 class="text-heading-md">{{ component.name }}</h3>
              </div>
              <span class="badge" :class="`badge-${component.status}`">
                {{ component.status }}
              </span>
            </div>

            <p class="text-body-sm text-gray-600 mb-4">
              {{ component.description }}
            </p>

            <div class="flex items-center justify-between">
              <span class="text-caption text-gray-500">v{{ component.version }}</span>
              <Button
                variant="outline"
                size="sm"
                @click="navigateTo(`/components/${component.slug}`)"
              >
                View Docs
              </Button>
            </div>
          </div>
        </div>
      </section>

      <section class="mb-16">
        <h2 class="text-heading-xl text-gray-900 mb-8">Live Examples</h2>

        <div class="space-y-8">
          <div class="bg-gray-50 rounded-lg p-6">
            <h3 class="text-heading-md mb-4">Buttons</h3>
            <div class="flex gap-4 flex-wrap">
              <Button variant="primary">Primary</Button>
              <Button variant="secondary">Secondary</Button>
              <Button variant="accent">Accent</Button>
              <Button variant="ghost">Ghost</Button>
              <Button variant="outline">Outline</Button>
            </div>
          </div>

          <div class="bg-gray-50 rounded-lg p-6">
            <h3 class="text-heading-md mb-4">Callouts</h3>
            <div class="space-y-4">
              <Callout type="info" title="Information">
                This is an informational message with some helpful details.
              </Callout>
              <Callout type="success" title="Success!" dismissible>
                Your changes have been saved successfully.
              </Callout>
              <Callout type="warning" title="Warning" dismissible>
                Please review your settings before continuing.
              </Callout>
              <Callout type="error" title="Error">
                Something went wrong. Please try again.
              </Callout>
            </div>
          </div>
        </div>
      </section>

      <section>
        <h2 class="text-heading-xl text-gray-900 mb-8">Notifications Demo</h2>
        <div class="flex gap-4 flex-wrap">
          <Button @click="showSuccessNotification">Show Success</Button>
          <Button @click="showErrorNotification" variant="outline">Show Error</Button>
          <Button @click="showWarningNotification" variant="ghost">Show Warning</Button>
          <Button @click="clearNotifications" variant="secondary">Clear All</Button>
        </div>
      </section>
    </div>

    <!-- Global notifications -->
    <div class="fixed top-4 right-4 space-y-2 z-50">
      <TransitionGroup name="notification" tag="div">
        <Callout
          v-for="notification in activeNotifications"
          :key="notification.id"
          :type="notification.type"
          :title="notification.title"
          dismissible
          class="min-w-80 max-w-md"
          @dismiss="removeNotification(notification.id)"
        >
          {{ notification.message }}
        </Callout>
      </TransitionGroup>
    </div>
  </div>
</template>

<script