Skip to content

Testing Strategies and Quality Assurance for Element Plus Applications

Overview

This guide covers comprehensive testing strategies and quality assurance practices for Element Plus applications, including unit testing, integration testing, end-to-end testing, and automated quality checks.

Testing Framework Setup

Vitest Configuration

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    
    // Coverage configuration
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'tests/',
        'dist/',
        '**/*.d.ts',
        '**/*.config.{js,ts}',
        '**/index.ts' // Re-export files
      ],
      thresholds: {
        global: {
          branches: 80,
          functions: 80,
          lines: 80,
          statements: 80
        }
      }
    },
    
    // Test file patterns
    include: [
      'src/**/*.{test,spec}.{js,ts,vue}',
      'tests/unit/**/*.{test,spec}.{js,ts,vue}'
    ],
    
    // Mock configuration
    deps: {
      inline: ['element-plus']
    }
  },
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@tests': resolve(__dirname, 'tests')
    }
  }
})
typescript
// tests/setup.ts
import { config } from '@vue/test-utils'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

// Global test configuration
config.global.plugins = [createPinia(), ElementPlus]

// Register Element Plus icons globally for tests
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  config.global.components[key] = component
}

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

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

// Setup global test utilities
global.console.warn = vi.fn()
global.console.error = vi.fn()

Unit Testing Strategies

Component Testing

typescript
// tests/unit/components/UserForm.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import { ElForm, ElFormItem, ElInput, ElButton } from 'element-plus'
import UserForm from '@/components/UserForm.vue'
import type { User } from '@/types/user'

describe('UserForm', () => {
  let wrapper: VueWrapper<any>
  
  const defaultProps = {
    user: {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
      role: 'user'
    } as User,
    loading: false
  }
  
  const createWrapper = (props = {}) => {
    return mount(UserForm, {
      props: { ...defaultProps, ...props },
      global: {
        components: {
          ElForm,
          ElFormItem,
          ElInput,
          ElButton
        }
      }
    })
  }
  
  beforeEach(() => {
    wrapper = createWrapper()
  })
  
  afterEach(() => {
    wrapper.unmount()
  })
  
  describe('Rendering', () => {
    it('should render form with user data', () => {
      expect(wrapper.find('[data-testid="user-name"]').element.value)
        .toBe('John Doe')
      expect(wrapper.find('[data-testid="user-email"]').element.value)
        .toBe('john@example.com')
    })
    
    it('should show loading state', async () => {
      await wrapper.setProps({ loading: true })
      
      const submitButton = wrapper.find('[data-testid="submit-button"]')
      expect(submitButton.attributes('loading')).toBeDefined()
      expect(submitButton.text()).toContain('Saving...')
    })
    
    it('should disable form when loading', async () => {
      await wrapper.setProps({ loading: true })
      
      const form = wrapper.findComponent(ElForm)
      expect(form.attributes('disabled')).toBeDefined()
    })
  })
  
  describe('Form Validation', () => {
    it('should validate required fields', async () => {
      const nameInput = wrapper.find('[data-testid="user-name"]')
      const emailInput = wrapper.find('[data-testid="user-email"]')
      
      await nameInput.setValue('')
      await emailInput.setValue('')
      
      const form = wrapper.findComponent(ElForm)
      const isValid = await form.vm.validate().catch(() => false)
      
      expect(isValid).toBe(false)
    })
    
    it('should validate email format', async () => {
      const emailInput = wrapper.find('[data-testid="user-email"]')
      await emailInput.setValue('invalid-email')
      
      const form = wrapper.findComponent(ElForm)
      const isValid = await form.vm.validate().catch(() => false)
      
      expect(isValid).toBe(false)
    })
    
    it('should pass validation with valid data', async () => {
      const form = wrapper.findComponent(ElForm)
      const isValid = await form.vm.validate().catch(() => false)
      
      expect(isValid).toBe(true)
    })
  })
  
  describe('Events', () => {
    it('should emit submit event with form data', async () => {
      const form = wrapper.findComponent(ElForm)
      await form.vm.validate()
      
      const submitButton = wrapper.find('[data-testid="submit-button"]')
      await submitButton.trigger('click')
      
      expect(wrapper.emitted('submit')).toBeTruthy()
      expect(wrapper.emitted('submit')[0]).toEqual([defaultProps.user])
    })
    
    it('should emit cancel event', async () => {
      const cancelButton = wrapper.find('[data-testid="cancel-button"]')
      await cancelButton.trigger('click')
      
      expect(wrapper.emitted('cancel')).toBeTruthy()
    })
    
    it('should not submit invalid form', async () => {
      const nameInput = wrapper.find('[data-testid="user-name"]')
      await nameInput.setValue('')
      
      const submitButton = wrapper.find('[data-testid="submit-button"]')
      await submitButton.trigger('click')
      
      expect(wrapper.emitted('submit')).toBeFalsy()
    })
  })
  
  describe('Accessibility', () => {
    it('should have proper ARIA labels', () => {
      const nameInput = wrapper.find('[data-testid="user-name"]')
      const emailInput = wrapper.find('[data-testid="user-email"]')
      
      expect(nameInput.attributes('aria-label')).toBe('User name')
      expect(emailInput.attributes('aria-label')).toBe('User email')
    })
    
    it('should have proper form structure', () => {
      const form = wrapper.find('form')
      expect(form.exists()).toBe(true)
      
      const fieldsets = wrapper.findAll('fieldset')
      expect(fieldsets.length).toBeGreaterThan(0)
    })
  })
})

Composables Testing

typescript
// tests/unit/composables/useApi.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ref } from 'vue'
import { useApi } from '@/composables/useApi'
import * as apiService from '@/services/api'

// Mock the API service
vi.mock('@/services/api', () => ({
  get: vi.fn(),
  post: vi.fn(),
  put: vi.fn(),
  delete: vi.fn()
}))

describe('useApi', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })
  
  describe('GET requests', () => {
    it('should handle successful GET request', async () => {
      const mockData = { id: 1, name: 'Test User' }
      vi.mocked(apiService.get).mockResolvedValue({ data: mockData })
      
      const { data, loading, error, execute } = useApi('/users/1')
      
      expect(loading.value).toBe(false)
      expect(data.value).toBeNull()
      expect(error.value).toBeNull()
      
      await execute()
      
      expect(loading.value).toBe(false)
      expect(data.value).toEqual(mockData)
      expect(error.value).toBeNull()
      expect(apiService.get).toHaveBeenCalledWith('/users/1')
    })
    
    it('should handle GET request errors', async () => {
      const mockError = new Error('Network error')
      vi.mocked(apiService.get).mockRejectedValue(mockError)
      
      const { data, loading, error, execute } = useApi('/users/1')
      
      await execute()
      
      expect(loading.value).toBe(false)
      expect(data.value).toBeNull()
      expect(error.value).toBe(mockError)
    })
    
    it('should set loading state during request', async () => {
      let resolvePromise: (value: any) => void
      const promise = new Promise(resolve => {
        resolvePromise = resolve
      })
      
      vi.mocked(apiService.get).mockReturnValue(promise)
      
      const { loading, execute } = useApi('/users/1')
      
      const executePromise = execute()
      
      expect(loading.value).toBe(true)
      
      resolvePromise({ data: {} })
      await executePromise
      
      expect(loading.value).toBe(false)
    })
  })
  
  describe('POST requests', () => {
    it('should handle successful POST request', async () => {
      const mockData = { id: 1, name: 'New User' }
      const postData = { name: 'New User', email: 'new@example.com' }
      
      vi.mocked(apiService.post).mockResolvedValue({ data: mockData })
      
      const { data, execute } = useApi('/users', {
        method: 'POST',
        immediate: false
      })
      
      await execute(postData)
      
      expect(data.value).toEqual(mockData)
      expect(apiService.post).toHaveBeenCalledWith('/users', postData)
    })
  })
  
  describe('Configuration options', () => {
    it('should execute immediately when immediate is true', async () => {
      const mockData = { id: 1, name: 'Test User' }
      vi.mocked(apiService.get).mockResolvedValue({ data: mockData })
      
      const { data } = useApi('/users/1', { immediate: true })
      
      // Wait for next tick
      await new Promise(resolve => setTimeout(resolve, 0))
      
      expect(apiService.get).toHaveBeenCalledWith('/users/1')
      expect(data.value).toEqual(mockData)
    })
    
    it('should not execute immediately when immediate is false', async () => {
      const { data } = useApi('/users/1', { immediate: false })
      
      await new Promise(resolve => setTimeout(resolve, 0))
      
      expect(apiService.get).not.toHaveBeenCalled()
      expect(data.value).toBeNull()
    })
    
    it('should transform response data', async () => {
      const mockData = { id: 1, name: 'Test User' }
      const transformedData = { userId: 1, userName: 'Test User' }
      
      vi.mocked(apiService.get).mockResolvedValue({ data: mockData })
      
      const { data, execute } = useApi('/users/1', {
        transform: (data: any) => ({
          userId: data.id,
          userName: data.name
        })
      })
      
      await execute()
      
      expect(data.value).toEqual(transformedData)
    })
  })
})

Store Testing

typescript
// tests/unit/stores/auth.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '@/stores/auth'
import * as authApi from '@/api/auth'
import type { User, LoginCredentials } from '@/types/auth'

// Mock the auth API
vi.mock('@/api/auth', () => ({
  login: vi.fn(),
  logout: vi.fn(),
  refreshToken: vi.fn(),
  getCurrentUser: vi.fn()
}))

// Mock localStorage
const localStorageMock = {
  getItem: vi.fn(),
  setItem: vi.fn(),
  removeItem: vi.fn(),
  clear: vi.fn()
}
Object.defineProperty(window, 'localStorage', {
  value: localStorageMock
})

describe('Auth Store', () => {
  let authStore: ReturnType<typeof useAuthStore>
  
  beforeEach(() => {
    setActivePinia(createPinia())
    authStore = useAuthStore()
    vi.clearAllMocks()
  })
  
  describe('Initial State', () => {
    it('should have correct initial state', () => {
      expect(authStore.user).toBeNull()
      expect(authStore.token).toBeNull()
      expect(authStore.isAuthenticated).toBe(false)
      expect(authStore.loading).toBe(false)
    })
  })
  
  describe('Login', () => {
    const mockUser: User = {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
      role: 'user'
    }
    
    const mockToken = 'mock-jwt-token'
    const credentials: LoginCredentials = {
      email: 'john@example.com',
      password: 'password123'
    }
    
    it('should login successfully', async () => {
      vi.mocked(authApi.login).mockResolvedValue({
        user: mockUser,
        token: mockToken
      })
      
      await authStore.login(credentials)
      
      expect(authStore.user).toEqual(mockUser)
      expect(authStore.token).toBe(mockToken)
      expect(authStore.isAuthenticated).toBe(true)
      expect(authStore.loading).toBe(false)
      
      expect(localStorageMock.setItem).toHaveBeenCalledWith('auth_token', mockToken)
      expect(authApi.login).toHaveBeenCalledWith(credentials)
    })
    
    it('should handle login failure', async () => {
      const error = new Error('Invalid credentials')
      vi.mocked(authApi.login).mockRejectedValue(error)
      
      await expect(authStore.login(credentials)).rejects.toThrow('Invalid credentials')
      
      expect(authStore.user).toBeNull()
      expect(authStore.token).toBeNull()
      expect(authStore.isAuthenticated).toBe(false)
      expect(authStore.loading).toBe(false)
    })
    
    it('should set loading state during login', async () => {
      let resolveLogin: (value: any) => void
      const loginPromise = new Promise(resolve => {
        resolveLogin = resolve
      })
      
      vi.mocked(authApi.login).mockReturnValue(loginPromise)
      
      const loginCall = authStore.login(credentials)
      
      expect(authStore.loading).toBe(true)
      
      resolveLogin({ user: mockUser, token: mockToken })
      await loginCall
      
      expect(authStore.loading).toBe(false)
    })
  })
  
  describe('Logout', () => {
    beforeEach(async () => {
      // Set up authenticated state
      authStore.$patch({
        user: { id: 1, name: 'John Doe', email: 'john@example.com', role: 'user' },
        token: 'mock-token'
      })
    })
    
    it('should logout successfully', async () => {
      vi.mocked(authApi.logout).mockResolvedValue(undefined)
      
      await authStore.logout()
      
      expect(authStore.user).toBeNull()
      expect(authStore.token).toBeNull()
      expect(authStore.isAuthenticated).toBe(false)
      
      expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token')
      expect(authApi.logout).toHaveBeenCalled()
    })
    
    it('should clear state even if API call fails', async () => {
      vi.mocked(authApi.logout).mockRejectedValue(new Error('Network error'))
      
      await authStore.logout()
      
      expect(authStore.user).toBeNull()
      expect(authStore.token).toBeNull()
      expect(authStore.isAuthenticated).toBe(false)
      expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token')
    })
  })
  
  describe('Token Refresh', () => {
    it('should refresh token successfully', async () => {
      const newToken = 'new-mock-token'
      vi.mocked(authApi.refreshToken).mockResolvedValue({ token: newToken })
      
      authStore.$patch({ token: 'old-token' })
      
      await authStore.refreshToken()
      
      expect(authStore.token).toBe(newToken)
      expect(localStorageMock.setItem).toHaveBeenCalledWith('auth_token', newToken)
    })
    
    it('should logout on refresh failure', async () => {
      vi.mocked(authApi.refreshToken).mockRejectedValue(new Error('Token expired'))
      
      authStore.$patch({ token: 'old-token' })
      
      await authStore.refreshToken()
      
      expect(authStore.user).toBeNull()
      expect(authStore.token).toBeNull()
      expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token')
    })
  })
  
  describe('Getters', () => {
    it('should compute isAuthenticated correctly', () => {
      expect(authStore.isAuthenticated).toBe(false)
      
      authStore.$patch({ token: 'mock-token' })
      expect(authStore.isAuthenticated).toBe(true)
      
      authStore.$patch({ token: null })
      expect(authStore.isAuthenticated).toBe(false)
    })
    
    it('should compute userRole correctly', () => {
      expect(authStore.userRole).toBeNull()
      
      authStore.$patch({
        user: { id: 1, name: 'John', email: 'john@example.com', role: 'admin' }
      })
      expect(authStore.userRole).toBe('admin')
    })
  })
})

Integration Testing

API Integration Tests

typescript
// tests/integration/api.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { setupServer } from 'msw/node'
import { rest } from 'msw'
import { userApi } from '@/api/users'
import type { User } from '@/types/user'

// Mock server setup
const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.json({
        data: [
          { id: 1, name: 'John Doe', email: 'john@example.com', role: 'user' },
          { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'admin' }
        ],
        total: 2,
        page: 1,
        limit: 10
      })
    )
  }),
  
  rest.get('/api/users/:id', (req, res, ctx) => {
    const { id } = req.params
    
    if (id === '1') {
      return res(
        ctx.json({
          data: { id: 1, name: 'John Doe', email: 'john@example.com', role: 'user' }
        })
      )
    }
    
    return res(ctx.status(404), ctx.json({ error: 'User not found' }))
  }),
  
  rest.post('/api/users', async (req, res, ctx) => {
    const userData = await req.json()
    
    return res(
      ctx.status(201),
      ctx.json({
        data: {
          id: 3,
          ...userData,
          createdAt: new Date().toISOString()
        }
      })
    )
  }),
  
  rest.put('/api/users/:id', async (req, res, ctx) => {
    const { id } = req.params
    const userData = await req.json()
    
    return res(
      ctx.json({
        data: {
          id: parseInt(id as string),
          ...userData,
          updatedAt: new Date().toISOString()
        }
      })
    )
  }),
  
  rest.delete('/api/users/:id', (req, res, ctx) => {
    return res(ctx.status(204))
  })
)

beforeAll(() => server.listen())
afterAll(() => server.close())

describe('User API Integration', () => {
  describe('GET /api/users', () => {
    it('should fetch users list', async () => {
      const response = await userApi.getUsers()
      
      expect(response.data).toHaveLength(2)
      expect(response.total).toBe(2)
      expect(response.data[0]).toMatchObject({
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
        role: 'user'
      })
    })
    
    it('should handle pagination parameters', async () => {
      const response = await userApi.getUsers({ page: 1, limit: 5 })
      
      expect(response.page).toBe(1)
      expect(response.limit).toBe(10) // Server returns default
    })
  })
  
  describe('GET /api/users/:id', () => {
    it('should fetch single user', async () => {
      const user = await userApi.getUser(1)
      
      expect(user).toMatchObject({
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
        role: 'user'
      })
    })
    
    it('should handle user not found', async () => {
      await expect(userApi.getUser(999)).rejects.toThrow('User not found')
    })
  })
  
  describe('POST /api/users', () => {
    it('should create new user', async () => {
      const newUser = {
        name: 'Bob Wilson',
        email: 'bob@example.com',
        role: 'user' as const
      }
      
      const createdUser = await userApi.createUser(newUser)
      
      expect(createdUser).toMatchObject({
        id: 3,
        ...newUser
      })
      expect(createdUser.createdAt).toBeDefined()
    })
  })
  
  describe('PUT /api/users/:id', () => {
    it('should update user', async () => {
      const updateData = {
        name: 'John Updated',
        email: 'john.updated@example.com',
        role: 'admin' as const
      }
      
      const updatedUser = await userApi.updateUser(1, updateData)
      
      expect(updatedUser).toMatchObject({
        id: 1,
        ...updateData
      })
      expect(updatedUser.updatedAt).toBeDefined()
    })
  })
  
  describe('DELETE /api/users/:id', () => {
    it('should delete user', async () => {
      await expect(userApi.deleteUser(1)).resolves.toBeUndefined()
    })
  })
})

Component Integration Tests

typescript
// tests/integration/UserManagement.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { ElTable, ElButton, ElDialog } from 'element-plus'
import UserManagement from '@/views/UserManagement.vue'
import { useUserStore } from '@/stores/users'
import * as userApi from '@/api/users'

// Mock the user API
vi.mock('@/api/users', () => ({
  getUsers: vi.fn(),
  createUser: vi.fn(),
  updateUser: vi.fn(),
  deleteUser: vi.fn()
}))

describe('UserManagement Integration', () => {
  let wrapper: any
  let userStore: any
  
  const mockUsers = [
    { id: 1, name: 'John Doe', email: 'john@example.com', role: 'user' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'admin' }
  ]
  
  beforeEach(() => {
    setActivePinia(createPinia())
    userStore = useUserStore()
    
    vi.mocked(userApi.getUsers).mockResolvedValue({
      data: mockUsers,
      total: 2,
      page: 1,
      limit: 10
    })
    
    wrapper = mount(UserManagement, {
      global: {
        components: {
          ElTable,
          ElButton,
          ElDialog
        }
      }
    })
  })
  
  afterEach(() => {
    wrapper.unmount()
    vi.clearAllMocks()
  })
  
  describe('Data Loading', () => {
    it('should load users on mount', async () => {
      await wrapper.vm.$nextTick()
      
      expect(userApi.getUsers).toHaveBeenCalled()
      expect(userStore.users).toEqual(mockUsers)
    })
    
    it('should display users in table', async () => {
      await wrapper.vm.$nextTick()
      
      const table = wrapper.findComponent(ElTable)
      expect(table.exists()).toBe(true)
      
      // Check if user data is displayed
      const tableRows = wrapper.findAll('[data-testid="user-row"]')
      expect(tableRows).toHaveLength(2)
      
      expect(tableRows[0].text()).toContain('John Doe')
      expect(tableRows[0].text()).toContain('john@example.com')
    })
  })
  
  describe('User Creation', () => {
    it('should open create dialog', async () => {
      const createButton = wrapper.find('[data-testid="create-user-button"]')
      await createButton.trigger('click')
      
      const dialog = wrapper.findComponent(ElDialog)
      expect(dialog.props('modelValue')).toBe(true)
    })
    
    it('should create new user', async () => {
      const newUser = {
        name: 'Bob Wilson',
        email: 'bob@example.com',
        role: 'user'
      }
      
      vi.mocked(userApi.createUser).mockResolvedValue({
        id: 3,
        ...newUser
      })
      
      // Open create dialog
      const createButton = wrapper.find('[data-testid="create-user-button"]')
      await createButton.trigger('click')
      
      // Fill form
      const nameInput = wrapper.find('[data-testid="user-name-input"]')
      const emailInput = wrapper.find('[data-testid="user-email-input"]')
      
      await nameInput.setValue(newUser.name)
      await emailInput.setValue(newUser.email)
      
      // Submit form
      const submitButton = wrapper.find('[data-testid="submit-user-button"]')
      await submitButton.trigger('click')
      
      expect(userApi.createUser).toHaveBeenCalledWith(newUser)
    })
  })
  
  describe('User Editing', () => {
    it('should open edit dialog with user data', async () => {
      await wrapper.vm.$nextTick()
      
      const editButton = wrapper.find('[data-testid="edit-user-1"]')
      await editButton.trigger('click')
      
      const dialog = wrapper.findComponent(ElDialog)
      expect(dialog.props('modelValue')).toBe(true)
      
      // Check if form is populated with user data
      const nameInput = wrapper.find('[data-testid="user-name-input"]')
      const emailInput = wrapper.find('[data-testid="user-email-input"]')
      
      expect(nameInput.element.value).toBe('John Doe')
      expect(emailInput.element.value).toBe('john@example.com')
    })
    
    it('should update user', async () => {
      const updatedUser = {
        name: 'John Updated',
        email: 'john.updated@example.com',
        role: 'admin'
      }
      
      vi.mocked(userApi.updateUser).mockResolvedValue({
        id: 1,
        ...updatedUser
      })
      
      await wrapper.vm.$nextTick()
      
      // Open edit dialog
      const editButton = wrapper.find('[data-testid="edit-user-1"]')
      await editButton.trigger('click')
      
      // Update form
      const nameInput = wrapper.find('[data-testid="user-name-input"]')
      const emailInput = wrapper.find('[data-testid="user-email-input"]')
      
      await nameInput.setValue(updatedUser.name)
      await emailInput.setValue(updatedUser.email)
      
      // Submit form
      const submitButton = wrapper.find('[data-testid="submit-user-button"]')
      await submitButton.trigger('click')
      
      expect(userApi.updateUser).toHaveBeenCalledWith(1, updatedUser)
    })
  })
  
  describe('User Deletion', () => {
    it('should delete user with confirmation', async () => {
      vi.mocked(userApi.deleteUser).mockResolvedValue(undefined)
      
      await wrapper.vm.$nextTick()
      
      const deleteButton = wrapper.find('[data-testid="delete-user-1"]')
      await deleteButton.trigger('click')
      
      // Confirm deletion
      const confirmButton = wrapper.find('[data-testid="confirm-delete"]')
      await confirmButton.trigger('click')
      
      expect(userApi.deleteUser).toHaveBeenCalledWith(1)
    })
  })
  
  describe('Error Handling', () => {
    it('should handle API errors gracefully', async () => {
      vi.mocked(userApi.getUsers).mockRejectedValue(new Error('Network error'))
      
      const wrapper = mount(UserManagement, {
        global: {
          components: { ElTable, ElButton, ElDialog }
        }
      })
      
      await wrapper.vm.$nextTick()
      
      // Check if error message is displayed
      const errorMessage = wrapper.find('[data-testid="error-message"]')
      expect(errorMessage.exists()).toBe(true)
      expect(errorMessage.text()).toContain('Failed to load users')
    })
  })
})

End-to-End Testing

Playwright Configuration

typescript
// 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/results.xml' }]
  ],
  
  use: {
    baseURL: 'http://localhost:5173',
    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:5173',
    reuseExistingServer: !process.env.CI
  }
})

E2E Test Examples

typescript
// tests/e2e/user-management.spec.ts
import { test, expect } from '@playwright/test'

test.describe('User Management', () => {
  test.beforeEach(async ({ page }) => {
    // Mock API responses
    await page.route('/api/users', async route => {
      await route.fulfill({
        json: {
          data: [
            { id: 1, name: 'John Doe', email: 'john@example.com', role: 'user' },
            { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'admin' }
          ],
          total: 2,
          page: 1,
          limit: 10
        }
      })
    })
    
    await page.goto('/users')
  })
  
  test('should display users list', async ({ page }) => {
    // Wait for the table to load
    await expect(page.locator('[data-testid="users-table"]')).toBeVisible()
    
    // Check if users are displayed
    await expect(page.locator('text=John Doe')).toBeVisible()
    await expect(page.locator('text=jane@example.com')).toBeVisible()
    
    // Check table structure
    const rows = page.locator('[data-testid="user-row"]')
    await expect(rows).toHaveCount(2)
  })
  
  test('should create new user', async ({ page }) => {
    // Mock create user API
    await page.route('/api/users', async route => {
      if (route.request().method() === 'POST') {
        const postData = route.request().postDataJSON()
        await route.fulfill({
          status: 201,
          json: {
            data: {
              id: 3,
              ...postData,
              createdAt: new Date().toISOString()
            }
          }
        })
      }
    })
    
    // Click create button
    await page.click('[data-testid="create-user-button"]')
    
    // Wait for dialog to open
    await expect(page.locator('.el-dialog')).toBeVisible()
    
    // Fill form
    await page.fill('[data-testid="user-name-input"]', 'Bob Wilson')
    await page.fill('[data-testid="user-email-input"]', 'bob@example.com')
    await page.selectOption('[data-testid="user-role-select"]', 'user')
    
    // Submit form
    await page.click('[data-testid="submit-user-button"]')
    
    // Wait for dialog to close
    await expect(page.locator('.el-dialog')).not.toBeVisible()
    
    // Check if success message is shown
    await expect(page.locator('.el-message--success')).toBeVisible()
  })
  
  test('should edit user', async ({ page }) => {
    // Mock update user API
    await page.route('/api/users/1', async route => {
      if (route.request().method() === 'PUT') {
        const putData = route.request().postDataJSON()
        await route.fulfill({
          json: {
            data: {
              id: 1,
              ...putData,
              updatedAt: new Date().toISOString()
            }
          }
        })
      }
    })
    
    // Click edit button for first user
    await page.click('[data-testid="edit-user-1"]')
    
    // Wait for dialog to open
    await expect(page.locator('.el-dialog')).toBeVisible()
    
    // Check if form is pre-filled
    await expect(page.locator('[data-testid="user-name-input"]')).toHaveValue('John Doe')
    
    // Update name
    await page.fill('[data-testid="user-name-input"]', 'John Updated')
    
    // Submit form
    await page.click('[data-testid="submit-user-button"]')
    
    // Wait for dialog to close
    await expect(page.locator('.el-dialog')).not.toBeVisible()
    
    // Check if success message is shown
    await expect(page.locator('.el-message--success')).toBeVisible()
  })
  
  test('should delete user', async ({ page }) => {
    // Mock delete user API
    await page.route('/api/users/1', async route => {
      if (route.request().method() === 'DELETE') {
        await route.fulfill({ status: 204 })
      }
    })
    
    // Click delete button
    await page.click('[data-testid="delete-user-1"]')
    
    // Wait for confirmation dialog
    await expect(page.locator('.el-message-box')).toBeVisible()
    
    // Confirm deletion
    await page.click('.el-message-box__btns .el-button--primary')
    
    // Wait for confirmation dialog to close
    await expect(page.locator('.el-message-box')).not.toBeVisible()
    
    // Check if success message is shown
    await expect(page.locator('.el-message--success')).toBeVisible()
  })
  
  test('should handle form validation', async ({ page }) => {
    // Click create button
    await page.click('[data-testid="create-user-button"]')
    
    // Wait for dialog to open
    await expect(page.locator('.el-dialog')).toBeVisible()
    
    // Try to submit empty form
    await page.click('[data-testid="submit-user-button"]')
    
    // Check for validation errors
    await expect(page.locator('.el-form-item__error')).toHaveCount(2)
    await expect(page.locator('text=Name is required')).toBeVisible()
    await expect(page.locator('text=Email is required')).toBeVisible()
    
    // Fill invalid email
    await page.fill('[data-testid="user-name-input"]', 'Test User')
    await page.fill('[data-testid="user-email-input"]', 'invalid-email')
    
    // Try to submit
    await page.click('[data-testid="submit-user-button"]')
    
    // Check for email validation error
    await expect(page.locator('text=Please enter a valid email')).toBeVisible()
  })
  
  test('should be responsive on mobile', async ({ page }) => {
    // Set mobile viewport
    await page.setViewportSize({ width: 375, height: 667 })
    
    // Check if mobile layout is applied
    await expect(page.locator('[data-testid="users-table"]')).toBeVisible()
    
    // Check if action buttons are properly sized
    const actionButtons = page.locator('[data-testid^="edit-user-"], [data-testid^="delete-user-"]')
    for (const button of await actionButtons.all()) {
      await expect(button).toBeVisible()
    }
  })
})

Visual Regression Testing

typescript
// tests/e2e/visual.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Visual Regression Tests', () => {
  test('should match user management page screenshot', async ({ page }) => {
    await page.goto('/users')
    
    // Wait for content to load
    await page.waitForSelector('[data-testid="users-table"]')
    
    // Take screenshot
    await expect(page).toHaveScreenshot('user-management.png')
  })
  
  test('should match create user dialog screenshot', async ({ page }) => {
    await page.goto('/users')
    
    // Open create dialog
    await page.click('[data-testid="create-user-button"]')
    await page.waitForSelector('.el-dialog')
    
    // Take screenshot of dialog
    await expect(page.locator('.el-dialog')).toHaveScreenshot('create-user-dialog.png')
  })
  
  test('should match form validation errors screenshot', async ({ page }) => {
    await page.goto('/users')
    
    // Open create dialog
    await page.click('[data-testid="create-user-button"]')
    await page.waitForSelector('.el-dialog')
    
    // Try to submit empty form to trigger validation
    await page.click('[data-testid="submit-user-button"]')
    
    // Wait for validation errors
    await page.waitForSelector('.el-form-item__error')
    
    // Take screenshot
    await expect(page.locator('.el-dialog')).toHaveScreenshot('form-validation-errors.png')
  })
})

Quality Assurance Automation

Pre-commit Hooks

json
// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
      "pre-push": "npm run test:unit && npm run type-check"
    }
  },
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,vue}": [
      "stylelint --fix"
    ],
    "*.{md,json}": [
      "prettier --write"
    ]
  }
}

Quality Gates

yaml
# .github/workflows/quality-gates.yml
name: Quality Gates

on:
  pull_request:
    branches: [main, develop]

jobs:
  quality-check:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'pnpm'
    
    - name: Install dependencies
      run: pnpm install --frozen-lockfile
    
    - name: Run linting
      run: pnpm run lint
    
    - name: Run type checking
      run: pnpm run type-check
    
    - name: Run unit tests with coverage
      run: pnpm run test:unit --coverage
    
    - name: Check coverage thresholds
      run: |
        COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
        if (( $(echo "$COVERAGE < 80" | bc -l) )); then
          echo "Coverage $COVERAGE% is below threshold of 80%"
          exit 1
        fi
    
    - name: Run security audit
      run: pnpm audit --audit-level moderate
    
    - name: Check bundle size
      run: |
        pnpm run build
        BUNDLE_SIZE=$(du -sb dist | cut -f1)
        MAX_SIZE=5242880  # 5MB
        if [ $BUNDLE_SIZE -gt $MAX_SIZE ]; then
          echo "Bundle size $BUNDLE_SIZE bytes exceeds maximum of $MAX_SIZE bytes"
          exit 1
        fi
    
    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

This comprehensive testing strategy ensures high-quality Element Plus applications through automated testing at multiple levels, from unit tests to end-to-end scenarios, with robust quality assurance processes.

Element Plus Study Guide