Reference

Reference Untitled

Untitled

OtherEvergreenPublic

Nuxt 3 Testing Configuration

Package.json Dependencies

{
  "name": "design-system-nuxt3",
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage",
    "test:ci": "vitest run --coverage",
    "test:e2e": "playwright test",
    "test:e2e:headed": "playwright test --headed",
    "test:e2e:debug": "playwright test --debug",
    "test:visual": "test-storybook",
    "test:a11y": "vitest --run --testPathPattern=a11y",
    "test:all": "npm run test:ci && npm run test:e2e && npm run test:visual",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "chromatic": "chromatic --exit-zero-on-changes",
    "bundlesize": "nuxi analyze",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "typecheck": "nuxt typecheck"
  },
  "devDependencies": {
    "@nuxt/devtools": "latest",
    "@nuxt/eslint-config": "^0.2.0",
    "@nuxt/test-utils": "^3.9.0",
    "@nuxt/ui": "^2.11.1",
    "@playwright/test": "^1.40.0",
    "@storybook/addon-a11y": "^7.6.6",
    "@storybook/addon-essentials": "^7.6.6",
    "@storybook/addon-interactions": "^7.6.6",
    "@storybook/addon-links": "^7.6.6",
    "@storybook/blocks": "^7.6.6",
    "@storybook/test-runner": "^0.16.0",
    "@storybook/testing-library": "^0.2.2",
    "@storybook/vue3": "^7.6.6",
    "@storybook/vue3-vite": "^7.6.6",
    "@vue/test-utils": "^2.4.3",
    "axe-playwright": "^1.2.3",
    "chromatic": "^10.1.0",
    "eslint": "^8.55.0",
    "happy-dom": "^12.10.3",
    "nuxt": "^3.9.0",
    "storybook": "^7.6.6",
    "typescript": "^5.3.3",
    "vitest": "^1.1.0",
    "vitest-axe": "^0.1.0",
    "@vitest/coverage-v8": "^1.1.0",
    "@vitest/ui": "^1.1.0"
  },
  "dependencies": {
    "@headlessui/vue": "^1.7.16",
    "@nuxtjs/tailwindcss": "^6.8.4",
    "@phosphor-icons/vue": "^2.1.6",
    "vue": "^3.3.13"
  }
}

Nuxt Configuration

File: nuxt.config.ts

export default defineNuxtConfig({
  devtools: { enabled: true },

  modules: [
    '@nuxtjs/tailwindcss',
    '@nuxt/ui',
    '@nuxt/test-utils/module'
  ],

  css: ['~/assets/css/main.css'],

  components: [
    {
      path: '~/components',
      pathPrefix: false,
    }
  ],

  typescript: {
    strict: true,
    typeCheck: true
  },

  // Testing configuration
  testUtils: {
    startOnBoot: false
  },

  // Build optimization for testing
  build: {
    analyze: {
      analyzerMode: 'static',
      openAnalyzer: false,
      generateStatsFile: true,
      statsFilename: 'stats.json'
    }
  },

  // Runtime config for testing
  runtimeConfig: {
    public: {
      testEnvironment: process.env.NODE_ENV === 'test'
    }
  },

  // Tailwind CSS configuration
  tailwindcss: {
    configPath: 'tailwind.config.ts',
    exposeConfig: true,
    viewer: true
  }
})

Vitest Configuration

File: vitest.config.ts

import vue from '@vitejs/plugin-vue'
import { resolve } from 'node:path'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',
    setupFiles: ['./tests/setup.ts'],
    include: [
      'components/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
      'composables/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
      'utils/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
      'tests/unit/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
    ],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: [
        'components/**/*.{vue,js,ts}',
        'composables/**/*.{js,ts}',
        'utils/**/*.{js,ts}'
      ],
      exclude: [
        'node_modules/',
        '.nuxt/',
        'tests/',
        '**/*.d.ts',
        '**/*.stories.{js,ts,vue}',
        '**/index.{js,ts}'
      ],
      thresholds: {
        global: {
          branches: 80,
          functions: 80,
          lines: 80,
          statements: 80
        }
      }
    },
    globals: true,
    transformMode: {
      web: [/\.[jt]sx?$/],
      ssr: [/\.vue$/]
    }
  },
  resolve: {
    alias: {
      '~': resolve(__dirname, '.'),
      '@': resolve(__dirname, '.'),
      '~~': resolve(__dirname, '.'),
      '@@': resolve(__dirname, '.')
    }
  },
  define: {
    __VUE_OPTIONS_API__: true,
    __VUE_PROD_DEVTOOLS__: false
  }
})

Test Setup

File: tests/setup.ts

import { config } from '@vue/test-utils'
import { createNuxtApp } from '#app'
import 'vitest-axe/extend-expect'

// Mock Nuxt composables
vi.mock('#app', () => ({
  useNuxtApp: () => ({
    $router: {
      push: vi.fn(),
      replace: vi.fn(),
      go: vi.fn(),
      back: vi.fn(),
      forward: vi.fn()
    }
  }),
  navigateTo: vi.fn(),
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    go: vi.fn(),
    back: vi.fn(),
    forward: vi.fn()
  }),
  useRoute: () => ({
    path: '/',
    params: {},
    query: {},
    hash: '',
    name: 'index'
  }),
  useHead: vi.fn(),
  useSeoMeta: vi.fn(),
  useState: vi.fn(),
  useRuntimeConfig: () => ({
    public: {
      testEnvironment: true
    }
  })
}))

// Mock Phosphor Icons
vi.mock('@phosphor-icons/vue', () => ({
  PhCheckCircle: { template: '<div data-testid="check-circle-icon" />' },
  PhWarning: { template: '<div data-testid="warning-icon" />' },
  PhXCircle: { template: '<div data-testid="x-circle-icon" />' },
  PhInfo: { template: '<div data-testid="info-icon" />' },
  PhSpinner: { template: '<div data-testid="spinner-icon" />' },
  PhCursorClick: { template: '<div data-testid="cursor-click-icon" />' }
}))

// Global test configuration
config.global.mocks = {
  $router: {
    push: vi.fn(),
    replace: vi.fn()
  },
  $route: {
    path: '/',
    params: {},
    query: {}
  }
}

// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
  constructor() {}
  observe() { return null }
  disconnect() { return null }
  unobserve() { return null }
}

// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
  constructor() {}
  observe() { return null }
  disconnect() { return null }
  unobserve() { return null }
}

// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
})

Button Component (Nuxt 3 Version)

File: components/Button.vue

<script setup lang="ts">
import { PhSpinner } from '@phosphor-icons/vue'
import { computed } from 'vue'

export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'accent' | 'ghost' | 'outline'
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
  disabled?: boolean
  loading?: boolean
  icon?: any
  type?: 'button' | 'submit' | 'reset'
  ariaLabel?: string
  testId?: string
}

interface ButtonEmits {
  click: [event: MouseEvent]
}

const props = withDefaults(defineProps<ButtonProps>(), {
  variant: 'primary',
  size: 'md',
  disabled: false,
  loading: false,
  type: 'button'
})

const emit = defineEmits<ButtonEmits>()

const buttonClasses = computed(() => {
  const baseClasses = 'btn'
  const variantClasses = `btn-${props.variant}`
  const sizeClasses = `btn-${props.size}`
  const stateClasses = [
    props.loading && 'btn-loading',
    props.disabled && 'btn-disabled'
  ].filter(Boolean).join(' ')

  return [baseClasses, variantClasses, sizeClasses, stateClasses]
    .filter(Boolean)
    .join(' ')
})

const iconSize = computed(() => {
  const sizeMap = {
    xs: 12,
    sm: 14,
    md: 16,
    lg: 18,
    xl: 20
  }
  return sizeMap[props.size]
})

function handleClick(event: MouseEvent) {
  if (!props.disabled && !props.loading) {
    emit('click', event)
  }
}
</script>

<template>
  <button
    :type="type"
    :class="buttonClasses"
    :disabled="disabled || loading"
    :aria-label="ariaLabel"
    :data-testid="testId"
    @click="handleClick"
  >
    <PhSpinner
      v-if="loading"
      class="btn-icon animate-spin"
      :size="iconSize"
    />
    <component
      :is="icon"
      v-else-if="icon"
      class="btn-icon"
      :size="iconSize"
    />

    <span class="btn-text">
      <slot />
    </span>
  </button>
</template>

<style scoped>
/* Button base styles */
.btn {
  @apply inline-flex items-center justify-center gap-2 px-4 py-2
         text-sm font-medium rounded-md transition-all duration-200
         focus:outline-none focus:ring-2 focus:ring-offset-2
         disabled:opacity-50 disabled:cursor-not-allowed;
}

/* Variant styles */
.btn-primary {
  @apply bg-primary text-white hover:bg-primary/90
         focus:ring-primary/50 active:bg-primary/95;
}

.btn-secondary {
  @apply bg-secondary text-white hover:bg-secondary/90
         focus:ring-secondary/50 active:bg-secondary/95;
}

.btn-accent {
  @apply bg-accent text-white hover:bg-accent/90
         focus:ring-accent/50 active:bg-accent/95;
}

.btn-ghost {
  @apply bg-transparent text-gray-700 hover:bg-gray-100
         focus:ring-gray-500/50 active:bg-gray-200;
}

.btn-outline {
  @apply bg-transparent border border-primary text-primary
         hover:bg-primary hover:text-white focus:ring-primary/50;
}

/* Size variants */
.btn-xs { @apply text-xs px-2 py-1; }
.btn-sm { @apply text-sm px-3 py-1.5; }
.btn-md { @apply text-sm px-4 py-2; }
.btn-lg { @apply text-base px-6 py-3; }
.btn-xl { @apply text-lg px-8 py-4; }

/* State styles */
.btn-loading {
  @apply cursor-wait;
}

.btn-icon {
  @apply flex-shrink-0;
}

.btn-text {
  @apply whitespace-nowrap;
}
</style>

Button Tests (Nuxt 3 Version)

File: components/Button.test.ts

import { PhCheckCircle } from '@phosphor-icons/vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { axe } from 'vitest-axe'

import Button from './Button.vue'

describe('Button Component', () => {
  describe('Rendering', () => {
    it('renders with default props', () => {
      const wrapper = mount(Button, {
        slots: {
          default: 'Click me'
        }
      })

      expect(wrapper.find('button').exists()).toBe(true)
      expect(wrapper.text()).toBe('Click me')
      expect(wrapper.classes()).toContain('btn')
      expect(wrapper.classes()).toContain('btn-primary')
      expect(wrapper.classes()).toContain('btn-md')
    })

    it('renders with custom variant', () => {
      const wrapper = mount(Button, {
        props: { variant: 'secondary' },
        slots: { default: 'Secondary' }
      })

      expect(wrapper.classes()).toContain('btn-secondary')
    })

    it('renders with custom size', () => {
      const wrapper = mount(Button, {
        props: { size: 'lg' },
        slots: { default: 'Large' }
      })

      expect(wrapper.classes()).toContain('btn-lg')
    })

    it('renders with icon', () => {
      const wrapper = mount(Button, {
        props: { icon: PhCheckCircle },
        slots: { default: 'With Icon' }
      })

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

  describe('States', () => {
    it('handles disabled state', () => {
      const wrapper = mount(Button, {
        props: { disabled: true },
        slots: { default: 'Disabled' }
      })

      const button = wrapper.find('button')
      expect(button.attributes('disabled')).toBeDefined()
      expect(wrapper.classes()).toContain('btn-disabled')
    })

    it('handles loading state', () => {
      const wrapper = mount(Button, {
        props: { loading: true },
        slots: { default: 'Loading' }
      })

      const button = wrapper.find('button')
      expect(button.attributes('disabled')).toBeDefined()
      expect(wrapper.classes()).toContain('btn-loading')
    })

    it('shows spinner in loading state', () => {
      const wrapper = mount(Button, {
        props: { loading: true },
        slots: { default: 'Loading' }
      })

      expect(wrapper.find('[data-testid="spinner-icon"]').exists()).toBe(true)
    })
  })

  describe('Interactions', () => {
    it('emits click events', async () => {
      const wrapper = mount(Button, {
        slots: { default: 'Click me' }
      })

      await wrapper.find('button').trigger('click')
      expect(wrapper.emitted().click).toBeTruthy()
      expect(wrapper.emitted().click).toHaveLength(1)
    })

    it('does not emit click when disabled', async () => {
      const wrapper = mount(Button, {
        props: { disabled: true },
        slots: { default: 'Disabled' }
      })

      await wrapper.find('button').trigger('click')
      expect(wrapper.emitted().click).toBeFalsy()
    })

    it('does not emit click when loading', async () => {
      const wrapper = mount(Button, {
        props: { loading: true },
        slots: { default: 'Loading' }
      })

      await wrapper.find('button').trigger('click')
      expect(wrapper.emitted().click).toBeFalsy()
    })

    it('handles keyboard events', async () => {
      const wrapper = mount(Button, {
        slots: { default: 'Press Enter' }
      })

      await wrapper.find('button').trigger('keydown.enter')
      // Note: Button doesn't emit on keydown, but we test the element receives the event
      expect(wrapper.find('button').exists()).toBe(true)
    })
  })

  describe('Accessibility', () => {
    it('should not have accessibility violations', async () => {
      const wrapper = mount(Button, {
        slots: { default: 'Accessible Button' }
      })

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

    it('supports custom aria-label', () => {
      const wrapper = mount(Button, {
        props: { ariaLabel: 'Custom label' },
        slots: { default: 'Icon only' }
      })

      expect(wrapper.find('button').attributes('aria-label')).toBe('Custom label')
    })

    it('supports test id', () => {
      const wrapper = mount(Button, {
        props: { testId: 'test-button' },
        slots: { default: 'Test Button' }
      })

      expect(wrapper.find('button').attributes('data-testid')).toBe('test-button')
    })
  })

  describe('Props validation', () => {
    it('accepts all valid variants', () => {
      const variants = ['primary', 'secondary', 'accent', 'ghost', 'outline']

      variants.forEach((variant) => {
        const wrapper = mount(Button, {
          props: { variant: variant as any },
          slots: { default: `${variant} button` }
        })

        expect(wrapper.classes()).toContain(`btn-${variant}`)
      })
    })

    it('accepts all valid sizes', () => {
      const sizes = ['xs', 'sm', 'md', 'lg', 'xl']

      sizes.forEach((size) => {
        const wrapper = mount(Button, {
          props: { size: size as any },
          slots: { default: `${size} button` }
        })

        expect(wrapper.classes()).toContain(`btn-${size}`)
      })
    })

    it('accepts valid button types', () => {
      const types = ['button', 'submit', 'reset']

      types.forEach((type) => {
        const wrapper = mount(Button, {
          props: { type: type as any },
          slots: { default: `${type} button` }
        })

        expect(wrapper.find('button').attributes('type')).toBe(type)
      })
    })
  })

  describe('Performance', () => {
    it('renders quickly', () => {
      const startTime = performance.now()

      mount(Button, {
        slots: { default: 'Performance Test' }
      })

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

      // Should render in under 16ms (60fps)
      expect(renderTime).toBeLessThan(16)
    })

    it('handles multiple instances efficiently', () => {
      const startTime = performance.now()

      // Mount multiple button instances
      for (let i = 0; i < 100; i++) {
        const wrapper = mount(Button, {
          slots: { default: `Button ${i}` }
        })
        wrapper.unmount()
      }

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

      // Should handle 100 instances in under 100ms
      expect(totalTime).toBeLessThan(100)
    })
  })
})

Storybook Configuration (Nuxt 3)

File: .storybook/main.ts

import type { StorybookConfig } from '@storybook/vue3-vite'

import { mergeConfig } from 'vite'

const config: StorybookConfig = {
  stories: [
    '../components/**/*.stories.@(js|jsx|mjs|ts|tsx|vue)',
    '../pages/**/*.stories.@(js|jsx|mjs|ts|tsx|vue)',
    '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|vue)'
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y'
  ],
  framework: {
    name: '@storybook/vue3-vite',
    options: {}
  },
  docs: {
    autodocs: 'tag'
  },
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: prop => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
    },
  },
  viteFinal: async (config) => {
    return mergeConfig(config, {
      define: {
        __VUE_OPTIONS_API__: true,
        __VUE_PROD_DEVTOOLS__: false
      },
      resolve: {
        alias: {
          '~': new URL('../', import.meta.url).pathname,
          '@': new URL('../', import.meta.url).pathname,
          '~~': new URL('../', import.meta.url).pathname,
          '@@': new URL('../', import.meta.url).pathname
        }
      }
    })
  }
}

export default config

File: .storybook/preview.ts

import type { Preview } from '@storybook/vue3'

import '../assets/css/main.css'

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    docs: {
      toc: true,
    },
    a11y: {
      config: {
        rules: [
          {
            id: 'color-contrast',
            enabled: true,
          },
          {
            id: 'focus-trap',
            enabled: true,
          },
        ],
      },
    },
    backgrounds: {
      default: 'light',
      values: [
        {
          name: 'light',
          value: '#ffffff',
        },
        {
          name: 'dark',
          value: '#1a1a1a',
        },
        {
          name: 'gray',
          value: '#f5f5f5',
        },
      ],
    },
    viewport: {
      viewports: {
        mobile: {
          name: 'Mobile',
          styles: { width: '375px', height: '667px' },
        },
        tablet: {
          name: 'Tablet',
          styles: { width: '768px', height: '1024px' },
        },
        desktop: {
          name: 'Desktop',
          styles: { width: '1200px', height: '800px' },
        },
      },
    },
  },
  tags: ['autodocs'],
}

export default preview

File: components/Button.stories.ts

import type { Meta, StoryObj } from '@storybook/vue3'

import { PhArrowRight, PhCheckCircle, PhDownload } from '@phosphor-icons/vue'

import Button from './Button.vue'

const meta: Meta<typeof Button> = {
  title: 'Elements/Button',
  component: Button,
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component: 'Interactive button element built with Vue 3 and Nuxt 3.',
      },
    },
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'accent', 'ghost', 'outline'],
    },
    size: {
      control: 'select',
      options: ['xs', 'sm', 'md', 'lg', 'xl'],
    },
    disabled: {
      control: 'boolean',
    },
    loading: {
      control: 'boolean',
    },
    onClick: { action: 'clicked' },
  },
  args: {
    onClick: () => console.log('Button clicked!'),
  },
}

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {
    variant: 'primary',
  },
  render: args => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args">Primary Button</Button>',
  }),
}

export const Secondary: Story = {
  args: {
    variant: 'secondary',
  },
  render: args => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args">Secondary Button</Button>',
  }),
}

export const WithIcon: Story = {
  args: {
    variant: 'primary',
    icon: PhCheckCircle,
  },
  render: args => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args">With Icon</Button>',
  }),
}

export const Loading: Story = {
  args: {
    variant: 'primary',
    loading: true,
  },
  render: args => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args">Loading...</Button>',
  }),
}

export const Disabled: Story = {
  args: {
    variant: 'primary',
    disabled: true,
  },
  render: args => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args">Disabled Button</Button>',
  }),
}

export const Sizes: Story = {
  render: () => ({
    components: { Button },
    template: `
      <div class="flex items-center gap-4">
        <Button size="xs">Extra Small</Button>
        <Button size="sm">Small</Button>
        <Button size="md">Medium</Button>
        <Button size="lg">Large</Button>
        <Button size="xl">Extra Large</Button>
      </div>
    `,
  }),
}

export const Variants: Story = {
  render: () => ({
    components: { Button },
    template: `
      <div class="flex items-center 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>
    `,
  }),
}

export const Interactive: Story = {
  render: () => ({
    components: { Button, PhDownload, PhArrowRight },
    setup() {
      const handleDownload = () => alert('Download started!')
      const handleLearnMore = () => alert('Navigation triggered!')
      return { handleDownload, handleLearnMore, PhDownload, PhArrowRight }
    },
    template: `
      <div class="space-y-4">
        <div class="flex gap-4">
          <Button
            variant="primary"
            :icon="PhDownload"
            @click="handleDownload"
          >
            Download File
          </Button>
          <Button
            variant="outline"
            :icon="PhArrowRight"
            @click="handleLearnMore"
          >
            Learn More
          </Button>
        </div>
      </div>
    `,
  }),
}

export const AccessibilityTest: Story = {
  args: {
    variant: 'primary',
    ariaLabel: 'This button demonstrates accessibility features',
  },
  render: args => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args">Accessible Button</Button>',
  }),
  parameters: {
    a11y: {
      config: {
        rules: [
          {
            id: 'color-contrast',
            enabled: true,
          },
        ],
      },
    },
  },
}

Playwright E2E Configuration (Nuxt 3)

File: playwright.config.ts

import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
    ['junit', { outputFile: 'test-results/junit.xml' }],
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
})

File: tests/e2e/button.spec.ts

import { expect, test } from '@playwright/test'
import { checkA11y, injectAxe } from 'axe-playwright'

test.describe('Button Component E2E (Nuxt 3)', () => {
  test.beforeEach(async ({ page }) => {
    // Navigate to a test page with buttons or Storybook
    await page.goto('/storybook-static/iframe.html?id=elements-button--primary')
    await injectAxe(page)
  })

  test('should render button correctly', async ({ page }) => {
    const button = page.getByRole('button', { name: 'Primary Button' })
    await expect(button).toBeVisible()
    await expect(button).toHaveClass(/btn-primary/)
  })

  test('should handle click interactions', async ({ page }) => {
    const button = page.getByRole('button')
    await button.click()

    // Verify click was registered
    await expect(button).toBeFocused()
  })

  test('should be keyboard accessible', async ({ page }) => {
    await page.keyboard.press('Tab')
    const button = page.getByRole('button')
    await expect(button).toBeFocused()

    await page.keyboard.press('Enter')
    // Verify keyboard activation
  })

  test('should handle loading state in Nuxt', async ({ page }) => {
    await page.goto('/storybook-static/iframe.html?id=elements-button--loading')

    const button = page.getByRole('button')
    await expect(button).toBeDisabled()
    await expect(button).toHaveClass(/btn-loading/)
  })

  test('should pass accessibility tests', async ({ page }) => {
    await checkA11y(page)
  })

  test('should work with Nuxt navigation', async ({ page }) => {
    // Test actual Nuxt page if you have one
    await page.goto('/')

    const button = page.getByRole('button').first()
    if (await button.isVisible()) {
      await button.click()
      // Test navigation or state changes specific to Nuxt
    }
  })

  test('should handle Vue 3 reactivity', async ({ page }) => {
    await page.goto('/storybook-static/iframe.html?id=elements-button--interactive')

    const downloadButton = page.getByRole('button', { name: /download/i })
    await downloadButton.click()

    // Test for Vue 3 reactive updates
    // You might check for state changes, emit events, etc.
  })

  test('should maintain performance with Nuxt hydration', async ({ page }) => {
    const startTime = Date.now()
    await page.goto('/')

    // Wait for Nuxt hydration to complete
    await page.waitForFunction(() => window.nuxt?.isHydrating === false, undefined, { timeout: 5000 })

    const endTime = Date.now()
    const hydrationTime = endTime - startTime

    // Hydration should complete quickly
    expect(hydrationTime).toBeLessThan(3000)
  })
})

Nuxt-specific Test Utilities

File: tests/utils/nuxt-test-utils.ts

import type { ComponentMountingOptions } from '@vue/test-utils'

import { mount, VueWrapper } from '@vue/test-utils'
import { createApp } from 'vue'

// Mock Nuxt app context
export function createNuxtTestApp() {
  return createApp({})
}

// Enhanced mount function with Nuxt context
export function mountWithNuxt<T>(component: T, options: ComponentMountingOptions<T> = {}): VueWrapper<any> {
  const defaultOptions = {
    global: {
      mocks: {
        $router: {
          push: vi.fn(),
          replace: vi.fn(),
          go: vi.fn(),
          back: vi.fn(),
          forward: vi.fn(),
          currentRoute: {
            value: {
              path: '/',
              params: {},
              query: {},
              hash: '',
              name: 'index'
            }
          }
        },
        $route: {
          path: '/',
          params: {},
          query: {},
          hash: '',
          name: 'index'
        },
        $nuxt: {
          isHydrating: false,
          payload: {},
          ssrContext: {},
          hook: vi.fn(),
          callHook: vi.fn()
        }
      },
      provide: {
        nuxtApp: {
          $router: {
            push: vi.fn(),
            replace: vi.fn()
          }
        }
      },
      stubs: {
        NuxtLink: {
          template: '<a><slot /></a>'
        },
        ClientOnly: {
          template: '<div><slot /></div>'
        }
      }
    }
  }

  return mount(component, {
    ...defaultOptions,
    ...options,
    global: {
      ...defaultOptions.global,
      ...options.global
    }
  })
}

// Nuxt composables mocking utilities
export function mockUseRoute(route = {}) {
  return {
    path: '/',
    params: {},
    query: {},
    hash: '',
    name: 'index',
    ...route
  }
}

export function mockUseRouter() {
  return {
    push: vi.fn(),
    replace: vi.fn(),
    go: vi.fn(),
    back: vi.fn(),
    forward: vi.fn(),
    beforeEach: vi.fn(),
    afterEach: vi.fn()
  }
}

export function mockUseNuxtApp() {
  return {
    $router: mockUseRouter(),
    hook: vi.fn(),
    callHook: vi.fn(),
    provide: vi.fn(),
    payload: {},
    ssrContext: {},
    isHydrating: false
  }
}

Accessibility Tests (Nuxt 3)

File: tests/a11y/button.a11y.test.ts

import { PhCheckCircle } from '@phosphor-icons/vue'
import { describe, expect, it } from 'vitest'
import { axe } from 'vitest-axe'

import Button from '../../components/Button.vue'
import { mountWithNuxt } from '../utils/nuxt-test-utils'

describe('Button Accessibility Tests (Nuxt 3)', () => {
  it('should not have accessibility violations - default', async () => {
    const wrapper = mountWithNuxt(Button, {
      slots: { default: 'Default Button' }
    })

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

  it('should not have accessibility violations - all variants', async () => {
    const variants = ['primary', 'secondary', 'accent', 'ghost', 'outline']

    for (const variant of variants) {
      const wrapper = mountWithNuxt(Button, {
        props: { variant: variant as any },
        slots: { default: `${variant} Button` }
      })

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

  it('should not have accessibility violations - disabled state', async () => {
    const wrapper = mountWithNuxt(Button, {
      props: { disabled: true },
      slots: { default: 'Disabled Button' }
    })

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

  it('should not have accessibility violations - loading state', async () => {
    const wrapper = mountWithNuxt(Button, {
      props: { loading: true },
      slots: { default: 'Loading Button' }
    })

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

  it('should not have accessibility violations - with Phosphor icon', async () => {
    const wrapper = mountWithNuxt(Button, {
      props: { icon: PhCheckCircle },
      slots: { default: 'With Icon' }
    })

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

  it('should handle focus properly in Nuxt context', async () => {
    const wrapper = mountWithNuxt(Button, {
      slots: { default: 'Focus Test' }
    })

    const results = await axe(wrapper.element, {
      rules: {
        'focus-order-semantics': { enabled: true },
        'focusable-content': { enabled: true },
      },
    })
    expect(results).toHaveNoViolations()
  })

  it('should work with Nuxt navigation accessibility', async () => {
    const wrapper = mountWithNuxt(Button, {
      props: {
        ariaLabel: 'Navigate to next page',
        onClick: () => {} // Would use navigateTo in real component
      },
      slots: { default: 'Next Page' }
    })

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

Performance Tests (Nuxt 3)

File: tests/performance/button.perf.test.ts

import { PhCheckCircle } from '@phosphor-icons/vue'
import { describe, expect, it } from 'vitest'

import Button from '../../components/Button.vue'
import { mountWithNuxt } from '../utils/nuxt-test-utils'

describe('Button Performance Tests (Nuxt 3)', () => {
  it('should render quickly with Nuxt context', () => {
    const startTime = performance.now()

    mountWithNuxt(Button, {
      slots: { default: 'Performance Test' }
    })

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

    // Should render in under 16ms (60fps) even with Nuxt context
    expect(renderTime).toBeLessThan(16)
  })

  it('should handle Vue 3 reactivity efficiently', async () => {
    const wrapper = mountWithNuxt(Button, {
      props: { variant: 'primary' },
      slots: { default: 'Reactive Test' }
    })

    const startTime = performance.now()

    // Test reactive prop changes
    await wrapper.setProps({ variant: 'secondary' })
    await wrapper.setProps({ size: 'lg' })
    await wrapper.setProps({ loading: true })

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

    // Reactive updates should be fast
    expect(updateTime).toBeLessThan(10)
  })

  it('should handle multiple button instances with Nuxt efficiently', () => {
    const startTime = performance.now()

    // Mount multiple button instances with Nuxt context
    const wrappers = []
    for (let i = 0; i < 50; i++) {
      const wrapper = mountWithNuxt(Button, {
        slots: { default: `Button ${i}` }
      })
      wrappers.push(wrapper)
    }

    // Cleanup
    wrappers.forEach(wrapper => wrapper.unmount())

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

    // Should handle 50 instances in under 50ms
    expect(totalTime).toBeLessThan(50)
  })

  it('should handle Phosphor icons efficiently', () => {
    const startTime = performance.now()

    // Mount buttons with different icons
    const iconButtons = []
    for (let i = 0; i < 20; i++) {
      const wrapper = mountWithNuxt(Button, {
        props: { icon: PhCheckCircle },
        slots: { default: `Icon Button ${i}` }
      })
      iconButtons.push(wrapper)
    }

    // Cleanup
    iconButtons.forEach(wrapper => wrapper.unmount())

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

    // Should render 20 icon buttons in under 30ms
    expect(renderTime).toBeLessThan(30)
  })

  it('should not cause memory leaks with Nuxt context', () => {
    const initialMemory = (performance as any).memory?.usedJSHeapSize || 0

    // Create and destroy many components with Nuxt context
    for (let i = 0; i < 500; i++) {
      const wrapper = mountWithNuxt(Button, {
        slots: { default: `Memory Test ${i}` }
      })
      wrapper.unmount()
    }

    // Force garbage collection if available
    if (global.gc) {
      global.gc()
    }

    const finalMemory = (performance as any).memory?.usedJSHeapSize || 0
    const memoryIncrease = finalMemory - initialMemory

    // Memory increase should be minimal (less than 2MB for Nuxt overhead)
    expect(memoryIncrease).toBeLessThan(2 * 1024 * 1024)
  })
})

Composable Tests

File: tests/composables/useButton.test.ts

// Example composable for button logic
import { describe, expect, it } from 'vitest'
import { nextTick, ref } from 'vue'

// Example composable (you would create this)
export function useButton(props: any) {
  const isLoading = ref(props.loading || false)
  const isDisabled = ref(props.disabled || false)

  const buttonClasses = computed(() => {
    const baseClasses = 'btn'
    const variantClasses = `btn-${props.variant || 'primary'}`
    const sizeClasses = `btn-${props.size || 'md'}`
    const stateClasses = [
      isLoading.value && 'btn-loading',
      isDisabled.value && 'btn-disabled'
    ].filter(Boolean).join(' ')

    return [baseClasses, variantClasses, sizeClasses, stateClasses]
      .filter(Boolean)
      .join(' ')
  })

  const handleClick = (event: MouseEvent, emit: any) => {
    if (!isDisabled.value && !isLoading.value) {
      emit('click', event)
    }
  }

  return {
    isLoading,
    isDisabled,
    buttonClasses,
    handleClick
  }
}

describe('useButton Composable', () => {
  it('should generate correct button classes', () => {
    const props = { variant: 'primary', size: 'md' }
    const { buttonClasses } = useButton(props)

    expect(buttonClasses.value).toBe('btn btn-primary btn-md')
  })

  it('should handle loading state', async () => {
    const props = { loading: true }
    const { buttonClasses, isLoading } = useButton(props)

    expect(isLoading.value).toBe(true)
    expect(buttonClasses.value).toContain('btn-loading')
  })

  it('should handle disabled state', async () => {
    const props = { disabled: true }
    const { buttonClasses, isDisabled } = useButton(props)

    expect(isDisabled.value).toBe(true)
    expect(buttonClasses.value).toContain('btn-disabled')
  })

  it('should prevent clicks when disabled', () => {
    const props = { disabled: true }
    const { handleClick } = useButton(props)
    const mockEmit = vi.fn()
    const mockEvent = new MouseEvent('click')

    handleClick(mockEvent, mockEmit)
    expect(mockEmit).not.toHaveBeenCalled()
  })

  it('should allow clicks when enabled', () => {
    const props = {}
    const { handleClick } = useButton(props)
    const mockEmit = vi.fn()
    const mockEvent = new MouseEvent('click')

    handleClick(mockEvent, mockEmit)
    expect(mockEmit).toHaveBeenCalledWith('click', mockEvent)
  })
})

GitHub Actions for Nuxt 3

File: .github/workflows/nuxt-test.yml

name: Nuxt 3 Design System Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run Nuxt typecheck
        run: npm run typecheck

      - name: Run unit tests
        run: npm run test:ci

      - name: Upload coverage reports
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20.x
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build Nuxt application
        run: npm run build

      - name: Test build artifacts
        run: |
          ls -la .output/
          test -f .output/nitro.json

  e2e-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20.x
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright
        run: npx playwright install --with-deps

      - name: Build Storybook
        run: npm run build-storybook

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

  visual-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20.x
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build Storybook
        run: npm run build-storybook

      - name: Run visual tests
        run: npm run test:visual

      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          token: ${{ secrets.GITHUB_TOKEN }}

  accessibility-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20.x
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run accessibility tests
        run: npm run test:a11y

      - name: Upload a11y results
        uses: actions/upload-artifact@v3
        with:
          name: accessibility-results
          path: a11y-results/

  bundle-analysis:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20.x
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build project
        run: npm run build

      - name: Analyze bundle
        run: npm run bundlesize

      - name: Upload bundle analysis
        uses: actions/upload-artifact@v3
        with:
          name: bundle-analysis
          path: .nuxt/analyze/

Tailwind Config for Testing

File: tailwind.config.ts

import type { Config } from 'tailwindcss'

export default <Config>{
  content: [
    './components/**/*.{js,vue,ts}',
    './layouts/**/*.vue',
    './pages/**/*.vue',
    './plugins/**/*.{js,ts}',
    './app.vue',
    './error.vue',
    './nuxt.config.{js,ts}',
    './.storybook/**/*.{js,ts}',
    './stories/**/*.{js,ts,vue}'
  ],
  theme: {
    extend: {
      colors: {
        // Brand colors
        primary: {
          DEFAULT: '#3b82f6',
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          900: '#1e3a8a',
        },
        secondary: {
          DEFAULT: '#6b7280',
          500: '#6b7280',
          600: '#4b5563',
          900: '#111827',
        },
        accent: {
          DEFAULT: '#8b5cf6',
          500: '#8b5cf6',
          600: '#7c3aed',
          900: '#4c1d95',
        },
        // Semantic colors
        success: {
          DEFAULT: '#10b981',
          500: '#10b981',
          600: '#059669',
          900: '#064e3b',
        },
        warning: {
          DEFAULT: '#f59e0b',
          500: '#f59e0b',
          600: '#d97706',
          900: '#78350f',
        },
        error: {
          DEFAULT: '#ef4444',
          500: '#ef4444',
          600: '#dc2626',
          900: '#7f1d1d',
        },
        info: {
          DEFAULT: '#06b6d4',
          500: '#06b6d4',
          600: '#0891b2',
          900: '#164e63',
        },
      },
    },
  },
  plugins: [],
}

VSCode Settings for Nuxt 3

File: .vscode/settings.json

{
  "typescript.preferences.includePackageJsonAutoImports": "auto",
  "typescript.suggest.autoImports": true,
  "vue.inlayHints.missingProps": true,
  "vue.inlayHints.inlineHandlerLeading": true,
  "vue.inlayHints.optionsWrapper": true,
  "nuxt.isNuxtApp": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "vitest.enable": true,
  "vitest.commandLine": "npm run test",
  "testing.automaticallyOpenPeekView": "never",
  "files.associations": {
    "*.test.ts": "typescript",
    "*.spec.ts": "typescript",
    "*.stories.ts": "typescript"
  },
  "emmet.includeLanguages": {
    "vue": "html"
  },
  "editor.quickSuggestions": {
    "strings": true
  }
}