Skip to content

Unit Testing with Element Plus ​

Overview ​

Testing Element Plus components requires a solid understanding of Vue testing patterns and the specific behaviors of UI components. This guide covers comprehensive testing strategies, from basic component testing to complex interaction scenarios.

Testing Setup ​

Dependencies ​

bash
# Vue Test Utils and Jest
npm install --save-dev @vue/test-utils jest vue-jest babel-jest

# For Vitest (recommended)
npm install --save-dev vitest @vue/test-utils jsdom

# Additional testing utilities
npm install --save-dev @testing-library/vue @testing-library/jest-dom
npm install --save-dev @testing-library/user-event

Vitest Configuration ​

javascript
// vitest.config.js
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.js']
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

Test Setup File ​

javascript
// tests/setup.js
import { config } from '@vue/test-utils'
import ElementPlus from 'element-plus'
import { createI18n } from 'vue-i18n'
import '@testing-library/jest-dom'

// Global test configuration
config.global.plugins = [ElementPlus]

// Mock i18n
const i18n = createI18n({
  locale: 'en',
  messages: {
    en: {
      test: {
        message: 'Test message'
      }
    }
  }
})

config.global.plugins.push(i18n)

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

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

// Mock window.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(),
  })),
})

Basic Component Testing ​

Testing Element Plus Components ​

javascript
// tests/components/BasicForm.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import BasicForm from '@/components/BasicForm.vue'

// Component under test
const BasicFormComponent = {
  template: `
    <el-form :model="form" :rules="rules" ref="formRef">
      <el-form-item label="Name" prop="name">
        <el-input v-model="form.name" data-testid="name-input" />
      </el-form-item>
      <el-form-item label="Email" prop="email">
        <el-input v-model="form.email" data-testid="email-input" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm" data-testid="submit-btn">
          Submit
        </el-button>
        <el-button @click="resetForm" data-testid="reset-btn">
          Reset
        </el-button>
      </el-form-item>
    </el-form>
  `,
  setup() {
    const form = ref({
      name: '',
      email: ''
    })
    
    const rules = {
      name: [
        { required: true, message: 'Please input name', trigger: 'blur' }
      ],
      email: [
        { required: true, message: 'Please input email', trigger: 'blur' },
        { type: 'email', message: 'Please input valid email', trigger: 'blur' }
      ]
    }
    
    const formRef = ref()
    
    const submitForm = async () => {
      await formRef.value.validate()
      // Submit logic
    }
    
    const resetForm = () => {
      formRef.value.resetFields()
    }
    
    return {
      form,
      rules,
      formRef,
      submitForm,
      resetForm
    }
  }
}

describe('BasicForm', () => {
  it('renders form fields correctly', () => {
    const wrapper = mount(BasicFormComponent)
    
    expect(wrapper.find('[data-testid="name-input"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="email-input"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="submit-btn"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="reset-btn"]').exists()).toBe(true)
  })
  
  it('updates form data when input changes', async () => {
    const wrapper = mount(BasicFormComponent)
    const nameInput = wrapper.find('[data-testid="name-input"] input')
    
    await nameInput.setValue('John Doe')
    
    expect(wrapper.vm.form.name).toBe('John Doe')
  })
  
  it('validates required fields', async () => {
    const wrapper = mount(BasicFormComponent)
    const submitBtn = wrapper.find('[data-testid="submit-btn"]')
    
    await submitBtn.trigger('click')
    
    // Check for validation error messages
    await wrapper.vm.$nextTick()
    expect(wrapper.text()).toContain('Please input name')
  })
  
  it('validates email format', async () => {
    const wrapper = mount(BasicFormComponent)
    const emailInput = wrapper.find('[data-testid="email-input"] input')
    
    await emailInput.setValue('invalid-email')
    await emailInput.trigger('blur')
    
    await wrapper.vm.$nextTick()
    expect(wrapper.text()).toContain('Please input valid email')
  })
  
  it('resets form when reset button is clicked', async () => {
    const wrapper = mount(BasicFormComponent)
    const nameInput = wrapper.find('[data-testid="name-input"] input')
    const resetBtn = wrapper.find('[data-testid="reset-btn"]')
    
    // Set some values
    await nameInput.setValue('John Doe')
    expect(wrapper.vm.form.name).toBe('John Doe')
    
    // Reset form
    await resetBtn.trigger('click')
    expect(wrapper.vm.form.name).toBe('')
  })
})

Testing with Testing Library ​

javascript
// tests/components/UserList.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/vue'
import { describe, it, expect, vi } from 'vitest'
import UserList from '@/components/UserList.vue'

const mockUsers = [
  { id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' }
]

// Mock API call
const mockFetchUsers = vi.fn().mockResolvedValue(mockUsers)

const UserListComponent = {
  template: `
    <div>
      <el-input
        v-model="searchTerm"
        placeholder="Search users..."
        @input="handleSearch"
        data-testid="search-input"
      />
      
      <el-table :data="filteredUsers" v-loading="loading">
        <el-table-column prop="name" label="Name" />
        <el-table-column prop="email" label="Email" />
        <el-table-column prop="status" label="Status">
          <template #default="{ row }">
            <el-tag :type="row.status === 'active' ? 'success' : 'danger'">
              {{ row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="Actions">
          <template #default="{ row }">
            <el-button
              size="small"
              @click="editUser(row)"
              :data-testid="`edit-user-${row.id}`"
            >
              Edit
            </el-button>
            <el-button
              size="small"
              type="danger"
              @click="deleteUser(row.id)"
              :data-testid="`delete-user-${row.id}`"
            >
              Delete
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  `,
  setup() {
    const users = ref([])
    const loading = ref(false)
    const searchTerm = ref('')
    
    const filteredUsers = computed(() => {
      if (!searchTerm.value) return users.value
      return users.value.filter(user => 
        user.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
        user.email.toLowerCase().includes(searchTerm.value.toLowerCase())
      )
    })
    
    const fetchUsers = async () => {
      loading.value = true
      try {
        users.value = await mockFetchUsers()
      } finally {
        loading.value = false
      }
    }
    
    const handleSearch = debounce(() => {
      // Search logic handled by computed property
    }, 300)
    
    const editUser = (user) => {
      console.log('Edit user:', user)
    }
    
    const deleteUser = (userId) => {
      users.value = users.value.filter(user => user.id !== userId)
    }
    
    onMounted(fetchUsers)
    
    return {
      users,
      loading,
      searchTerm,
      filteredUsers,
      handleSearch,
      editUser,
      deleteUser
    }
  }
}

describe('UserList', () => {
  it('renders user list correctly', async () => {
    render(UserListComponent)
    
    // Wait for users to load
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
      expect(screen.getByText('Jane Smith')).toBeInTheDocument()
    })
  })
  
  it('filters users based on search term', async () => {
    render(UserListComponent)
    
    // Wait for initial load
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
    })
    
    // Search for specific user
    const searchInput = screen.getByTestId('search-input')
    await fireEvent.update(searchInput, 'John')
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
      expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument()
    })
  })
  
  it('deletes user when delete button is clicked', async () => {
    render(UserListComponent)
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument()
    })
    
    // Click delete button
    const deleteBtn = screen.getByTestId('delete-user-1')
    await fireEvent.click(deleteBtn)
    
    await waitFor(() => {
      expect(screen.queryByText('John Doe')).not.toBeInTheDocument()
    })
  })
  
  it('shows loading state', () => {
    const wrapper = render(UserListComponent)
    
    // Initially should show loading
    expect(wrapper.container.querySelector('.el-loading-mask')).toBeInTheDocument()
  })
})

Testing Complex Interactions ​

Testing Form Validation ​

javascript
// tests/components/AdvancedForm.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { nextTick } from 'vue'

const AdvancedFormComponent = {
  template: `
    <el-form :model="form" :rules="rules" ref="formRef">
      <el-form-item label="Username" prop="username">
        <el-input v-model="form.username" data-testid="username" />
      </el-form-item>
      
      <el-form-item label="Password" prop="password">
        <el-input
          v-model="form.password"
          type="password"
          data-testid="password"
        />
      </el-form-item>
      
      <el-form-item label="Confirm Password" prop="confirmPassword">
        <el-input
          v-model="form.confirmPassword"
          type="password"
          data-testid="confirm-password"
        />
      </el-form-item>
      
      <el-form-item label="Email" prop="email">
        <el-input v-model="form.email" data-testid="email" />
      </el-form-item>
      
      <el-form-item label="Age" prop="age">
        <el-input-number
          v-model="form.age"
          :min="18"
          :max="100"
          data-testid="age"
        />
      </el-form-item>
      
      <el-form-item label="Gender" prop="gender">
        <el-select v-model="form.gender" data-testid="gender">
          <el-option label="Male" value="male" />
          <el-option label="Female" value="female" />
          <el-option label="Other" value="other" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="Interests" prop="interests">
        <el-checkbox-group v-model="form.interests">
          <el-checkbox label="sports" data-testid="interest-sports">Sports</el-checkbox>
          <el-checkbox label="music" data-testid="interest-music">Music</el-checkbox>
          <el-checkbox label="reading" data-testid="interest-reading">Reading</el-checkbox>
        </el-checkbox-group>
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="submitForm" data-testid="submit">
          Submit
        </el-button>
      </el-form-item>
    </el-form>
  `,
  setup() {
    const form = ref({
      username: '',
      password: '',
      confirmPassword: '',
      email: '',
      age: null,
      gender: '',
      interests: []
    })
    
    const validatePassword = (rule, value, callback) => {
      if (value === '') {
        callback(new Error('Please input password'))
      } else if (value.length < 6) {
        callback(new Error('Password must be at least 6 characters'))
      } else {
        callback()
      }
    }
    
    const validateConfirmPassword = (rule, value, callback) => {
      if (value === '') {
        callback(new Error('Please confirm password'))
      } else if (value !== form.value.password) {
        callback(new Error('Passwords do not match'))
      } else {
        callback()
      }
    }
    
    const rules = {
      username: [
        { required: true, message: 'Please input username', trigger: 'blur' },
        { min: 3, max: 20, message: 'Length should be 3 to 20', trigger: 'blur' }
      ],
      password: [
        { validator: validatePassword, trigger: 'blur' }
      ],
      confirmPassword: [
        { validator: validateConfirmPassword, trigger: 'blur' }
      ],
      email: [
        { required: true, message: 'Please input email', trigger: 'blur' },
        { type: 'email', message: 'Please input valid email', trigger: 'blur' }
      ],
      age: [
        { required: true, message: 'Please input age', trigger: 'blur' },
        { type: 'number', min: 18, max: 100, message: 'Age must be between 18 and 100', trigger: 'blur' }
      ],
      gender: [
        { required: true, message: 'Please select gender', trigger: 'change' }
      ],
      interests: [
        { type: 'array', min: 1, message: 'Please select at least one interest', trigger: 'change' }
      ]
    }
    
    const formRef = ref()
    
    const submitForm = async () => {
      try {
        await formRef.value.validate()
        console.log('Form submitted:', form.value)
      } catch (error) {
        console.log('Validation failed:', error)
      }
    }
    
    return {
      form,
      rules,
      formRef,
      submitForm
    }
  }
}

describe('AdvancedForm Validation', () => {
  it('validates required fields', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const submitBtn = wrapper.find('[data-testid="submit"]')
    
    await submitBtn.trigger('click')
    await nextTick()
    
    // Check for validation error messages
    expect(wrapper.text()).toContain('Please input username')
    expect(wrapper.text()).toContain('Please input password')
    expect(wrapper.text()).toContain('Please input email')
  })
  
  it('validates password length', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const passwordInput = wrapper.find('[data-testid="password"] input')
    
    await passwordInput.setValue('123')
    await passwordInput.trigger('blur')
    await nextTick()
    
    expect(wrapper.text()).toContain('Password must be at least 6 characters')
  })
  
  it('validates password confirmation', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const passwordInput = wrapper.find('[data-testid="password"] input')
    const confirmPasswordInput = wrapper.find('[data-testid="confirm-password"] input')
    
    await passwordInput.setValue('password123')
    await confirmPasswordInput.setValue('different')
    await confirmPasswordInput.trigger('blur')
    await nextTick()
    
    expect(wrapper.text()).toContain('Passwords do not match')
  })
  
  it('validates email format', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const emailInput = wrapper.find('[data-testid="email"] input')
    
    await emailInput.setValue('invalid-email')
    await emailInput.trigger('blur')
    await nextTick()
    
    expect(wrapper.text()).toContain('Please input valid email')
  })
  
  it('validates age range', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const ageInput = wrapper.find('[data-testid="age"] input')
    
    await ageInput.setValue('15')
    await ageInput.trigger('blur')
    await nextTick()
    
    expect(wrapper.text()).toContain('Age must be between 18 and 100')
  })
  
  it('validates checkbox group selection', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const submitBtn = wrapper.find('[data-testid="submit"]')
    
    // Fill other required fields
    await wrapper.find('[data-testid="username"] input').setValue('testuser')
    await wrapper.find('[data-testid="password"] input').setValue('password123')
    await wrapper.find('[data-testid="confirm-password"] input').setValue('password123')
    await wrapper.find('[data-testid="email"] input').setValue('test@example.com')
    await wrapper.find('[data-testid="age"] input').setValue('25')
    
    // Select gender
    const genderSelect = wrapper.findComponent({ name: 'ElSelect' })
    await genderSelect.vm.$emit('update:modelValue', 'male')
    
    await submitBtn.trigger('click')
    await nextTick()
    
    expect(wrapper.text()).toContain('Please select at least one interest')
  })
  
  it('submits form with valid data', async () => {
    const wrapper = mount(AdvancedFormComponent)
    const consoleSpy = vi.spyOn(console, 'log')
    
    // Fill all fields with valid data
    await wrapper.find('[data-testid="username"] input').setValue('testuser')
    await wrapper.find('[data-testid="password"] input').setValue('password123')
    await wrapper.find('[data-testid="confirm-password"] input').setValue('password123')
    await wrapper.find('[data-testid="email"] input').setValue('test@example.com')
    await wrapper.find('[data-testid="age"] input').setValue('25')
    
    // Select gender
    const genderSelect = wrapper.findComponent({ name: 'ElSelect' })
    await genderSelect.vm.$emit('update:modelValue', 'male')
    
    // Select interests
    const sportsCheckbox = wrapper.find('[data-testid="interest-sports"] input')
    await sportsCheckbox.setChecked(true)
    
    const submitBtn = wrapper.find('[data-testid="submit"]')
    await submitBtn.trigger('click')
    await nextTick()
    
    expect(consoleSpy).toHaveBeenCalledWith('Form submitted:', expect.objectContaining({
      username: 'testuser',
      email: 'test@example.com',
      age: 25,
      gender: 'male',
      interests: ['sports']
    }))
  })
})

Testing Dialog and Modal Interactions ​

javascript
// tests/components/UserDialog.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { nextTick } from 'vue'

const UserDialogComponent = {
  template: `
    <div>
      <el-button @click="showDialog" data-testid="open-dialog">
        Add User
      </el-button>
      
      <el-dialog
        v-model="dialogVisible"
        title="User Information"
        width="500px"
        :before-close="handleClose"
      >
        <el-form :model="form" :rules="rules" ref="formRef">
          <el-form-item label="Name" prop="name">
            <el-input v-model="form.name" data-testid="dialog-name" />
          </el-form-item>
          <el-form-item label="Email" prop="email">
            <el-input v-model="form.email" data-testid="dialog-email" />
          </el-form-item>
        </el-form>
        
        <template #footer>
          <el-button @click="handleCancel" data-testid="cancel-btn">
            Cancel
          </el-button>
          <el-button
            type="primary"
            @click="handleConfirm"
            :loading="loading"
            data-testid="confirm-btn"
          >
            Confirm
          </el-button>
        </template>
      </el-dialog>
    </div>
  `,
  setup() {
    const dialogVisible = ref(false)
    const loading = ref(false)
    const form = ref({
      name: '',
      email: ''
    })
    
    const rules = {
      name: [
        { required: true, message: 'Please input name', trigger: 'blur' }
      ],
      email: [
        { required: true, message: 'Please input email', trigger: 'blur' },
        { type: 'email', message: 'Please input valid email', trigger: 'blur' }
      ]
    }
    
    const formRef = ref()
    
    const showDialog = () => {
      dialogVisible.value = true
    }
    
    const handleClose = (done) => {
      if (form.value.name || form.value.email) {
        ElMessageBox.confirm(
          'You have unsaved changes. Are you sure you want to close?',
          'Warning',
          {
            confirmButtonText: 'Yes',
            cancelButtonText: 'No',
            type: 'warning'
          }
        ).then(() => {
          resetForm()
          done()
        }).catch(() => {})
      } else {
        done()
      }
    }
    
    const handleCancel = () => {
      dialogVisible.value = false
      resetForm()
    }
    
    const handleConfirm = async () => {
      try {
        await formRef.value.validate()
        loading.value = true
        
        // Simulate API call
        await new Promise(resolve => setTimeout(resolve, 1000))
        
        console.log('User saved:', form.value)
        dialogVisible.value = false
        resetForm()
      } catch (error) {
        console.log('Validation failed')
      } finally {
        loading.value = false
      }
    }
    
    const resetForm = () => {
      form.value = { name: '', email: '' }
      if (formRef.value) {
        formRef.value.clearValidate()
      }
    }
    
    return {
      dialogVisible,
      loading,
      form,
      rules,
      formRef,
      showDialog,
      handleClose,
      handleCancel,
      handleConfirm
    }
  }
}

describe('UserDialog', () => {
  it('opens dialog when button is clicked', async () => {
    const wrapper = mount(UserDialogComponent)
    const openBtn = wrapper.find('[data-testid="open-dialog"]')
    
    expect(wrapper.vm.dialogVisible).toBe(false)
    
    await openBtn.trigger('click')
    
    expect(wrapper.vm.dialogVisible).toBe(true)
  })
  
  it('closes dialog when cancel is clicked', async () => {
    const wrapper = mount(UserDialogComponent)
    
    // Open dialog
    wrapper.vm.dialogVisible = true
    await nextTick()
    
    const cancelBtn = wrapper.find('[data-testid="cancel-btn"]')
    await cancelBtn.trigger('click')
    
    expect(wrapper.vm.dialogVisible).toBe(false)
  })
  
  it('validates form before confirming', async () => {
    const wrapper = mount(UserDialogComponent)
    const consoleSpy = vi.spyOn(console, 'log')
    
    // Open dialog
    wrapper.vm.dialogVisible = true
    await nextTick()
    
    const confirmBtn = wrapper.find('[data-testid="confirm-btn"]')
    await confirmBtn.trigger('click')
    await nextTick()
    
    // Should show validation errors
    expect(wrapper.text()).toContain('Please input name')
    expect(consoleSpy).toHaveBeenCalledWith('Validation failed')
  })
  
  it('submits form with valid data', async () => {
    const wrapper = mount(UserDialogComponent)
    const consoleSpy = vi.spyOn(console, 'log')
    
    // Open dialog
    wrapper.vm.dialogVisible = true
    await nextTick()
    
    // Fill form
    const nameInput = wrapper.find('[data-testid="dialog-name"] input')
    const emailInput = wrapper.find('[data-testid="dialog-email"] input')
    
    await nameInput.setValue('John Doe')
    await emailInput.setValue('john@example.com')
    
    const confirmBtn = wrapper.find('[data-testid="confirm-btn"]')
    await confirmBtn.trigger('click')
    
    // Should show loading state
    expect(wrapper.vm.loading).toBe(true)
    
    // Wait for async operation
    await new Promise(resolve => setTimeout(resolve, 1100))
    
    expect(consoleSpy).toHaveBeenCalledWith('User saved:', {
      name: 'John Doe',
      email: 'john@example.com'
    })
    expect(wrapper.vm.dialogVisible).toBe(false)
  })
})

Testing Async Operations ​

Testing API Calls ​

javascript
// tests/composables/useUserApi.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useUserApi } from '@/composables/useUserApi'

// Mock fetch
global.fetch = vi.fn()

const mockUsers = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]

describe('useUserApi', () => {
  beforeEach(() => {
    fetch.mockClear()
  })
  
  it('fetches users successfully', async () => {
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ data: mockUsers, total: 2 })
    })
    
    const { users, loading, error, fetchUsers } = useUserApi()
    
    expect(loading.value).toBe(false)
    expect(users.value).toEqual([])
    
    await fetchUsers()
    
    expect(fetch).toHaveBeenCalledWith('/api/users')
    expect(users.value).toEqual(mockUsers)
    expect(loading.value).toBe(false)
    expect(error.value).toBe(null)
  })
  
  it('handles fetch error', async () => {
    const errorMessage = 'Failed to fetch'
    fetch.mockRejectedValueOnce(new Error(errorMessage))
    
    const { users, loading, error, fetchUsers } = useUserApi()
    
    await fetchUsers()
    
    expect(users.value).toEqual([])
    expect(loading.value).toBe(false)
    expect(error.value).toBe(errorMessage)
  })
  
  it('creates user successfully', async () => {
    const newUser = { name: 'New User', email: 'new@example.com' }
    const createdUser = { id: 3, ...newUser }
    
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => createdUser
    })
    
    const { createUser } = useUserApi()
    const result = await createUser(newUser)
    
    expect(fetch).toHaveBeenCalledWith('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newUser)
    })
    expect(result).toEqual(createdUser)
  })
  
  it('updates user successfully', async () => {
    const userId = 1
    const updates = { name: 'Updated Name' }
    const updatedUser = { id: userId, name: 'Updated Name', email: 'john@example.com' }
    
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => updatedUser
    })
    
    const { updateUser } = useUserApi()
    const result = await updateUser(userId, updates)
    
    expect(fetch).toHaveBeenCalledWith(`/api/users/${userId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates)
    })
    expect(result).toEqual(updatedUser)
  })
  
  it('deletes user successfully', async () => {
    const userId = 1
    
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ success: true })
    })
    
    const { deleteUser } = useUserApi()
    const result = await deleteUser(userId)
    
    expect(fetch).toHaveBeenCalledWith(`/api/users/${userId}`, {
      method: 'DELETE'
    })
    expect(result).toEqual({ success: true })
  })
})

Testing Loading States ​

javascript
// tests/components/AsyncComponent.test.js
import { mount, flushPromises } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'

const AsyncComponentTest = {
  template: `
    <div>
      <el-button @click="loadData" :loading="loading" data-testid="load-btn">
        Load Data
      </el-button>
      
      <div v-if="loading" data-testid="loading-state">
        <el-skeleton :rows="3" animated />
      </div>
      
      <div v-else-if="error" data-testid="error-state">
        <el-alert type="error" :title="error" show-icon />
      </div>
      
      <div v-else-if="data.length > 0" data-testid="data-state">
        <el-card v-for="item in data" :key="item.id">
          <h3>{{ item.title }}</h3>
          <p>{{ item.description }}</p>
        </el-card>
      </div>
      
      <div v-else data-testid="empty-state">
        <el-empty description="No data available" />
      </div>
    </div>
  `,
  setup() {
    const data = ref([])
    const loading = ref(false)
    const error = ref(null)
    
    const loadData = async () => {
      loading.value = true
      error.value = null
      
      try {
        // Simulate API call
        await new Promise(resolve => setTimeout(resolve, 1000))
        
        if (Math.random() > 0.7) {
          throw new Error('Random error occurred')
        }
        
        data.value = [
          { id: 1, title: 'Item 1', description: 'Description 1' },
          { id: 2, title: 'Item 2', description: 'Description 2' }
        ]
      } catch (err) {
        error.value = err.message
      } finally {
        loading.value = false
      }
    }
    
    return {
      data,
      loading,
      error,
      loadData
    }
  }
}

describe('AsyncComponent', () => {
  it('shows initial empty state', () => {
    const wrapper = mount(AsyncComponentTest)
    
    expect(wrapper.find('[data-testid="empty-state"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="loading-state"]').exists()).toBe(false)
    expect(wrapper.find('[data-testid="error-state"]').exists()).toBe(false)
    expect(wrapper.find('[data-testid="data-state"]').exists()).toBe(false)
  })
  
  it('shows loading state when loading data', async () => {
    const wrapper = mount(AsyncComponentTest)
    const loadBtn = wrapper.find('[data-testid="load-btn"]')
    
    await loadBtn.trigger('click')
    
    expect(wrapper.find('[data-testid="loading-state"]').exists()).toBe(true)
    expect(wrapper.vm.loading).toBe(true)
  })
  
  it('shows data state after successful load', async () => {
    // Mock Math.random to ensure success
    const originalRandom = Math.random
    Math.random = vi.fn(() => 0.5) // Less than 0.7, so no error
    
    const wrapper = mount(AsyncComponentTest)
    const loadBtn = wrapper.find('[data-testid="load-btn"]')
    
    await loadBtn.trigger('click')
    
    // Wait for async operation to complete
    await new Promise(resolve => setTimeout(resolve, 1100))
    await flushPromises()
    
    expect(wrapper.find('[data-testid="data-state"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="loading-state"]').exists()).toBe(false)
    expect(wrapper.text()).toContain('Item 1')
    expect(wrapper.text()).toContain('Item 2')
    
    // Restore Math.random
    Math.random = originalRandom
  })
  
  it('shows error state when load fails', async () => {
    // Mock Math.random to ensure error
    const originalRandom = Math.random
    Math.random = vi.fn(() => 0.8) // Greater than 0.7, so error occurs
    
    const wrapper = mount(AsyncComponentTest)
    const loadBtn = wrapper.find('[data-testid="load-btn"]')
    
    await loadBtn.trigger('click')
    
    // Wait for async operation to complete
    await new Promise(resolve => setTimeout(resolve, 1100))
    await flushPromises()
    
    expect(wrapper.find('[data-testid="error-state"]').exists()).toBe(true)
    expect(wrapper.find('[data-testid="loading-state"]').exists()).toBe(false)
    expect(wrapper.text()).toContain('Random error occurred')
    
    // Restore Math.random
    Math.random = originalRandom
  })
})

Testing Utilities and Helpers ​

Custom Test Utilities ​

javascript
// tests/utils/test-utils.js
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import ElementPlus from 'element-plus'

// Create test wrapper with common setup
export function createWrapper(component, options = {}) {
  const i18n = createI18n({
    locale: 'en',
    messages: {
      en: {
        test: 'Test message'
      }
    }
  })
  
  return mount(component, {
    global: {
      plugins: [ElementPlus, i18n],
      ...options.global
    },
    ...options
  })
}

// Wait for Element Plus components to be ready
export async function waitForElement(wrapper, selector, timeout = 1000) {
  const start = Date.now()
  
  while (Date.now() - start < timeout) {
    if (wrapper.find(selector).exists()) {
      return wrapper.find(selector)
    }
    await new Promise(resolve => setTimeout(resolve, 10))
  }
  
  throw new Error(`Element ${selector} not found within ${timeout}ms`)
}

// Trigger Element Plus component events
export async function triggerElEvent(wrapper, componentName, event, payload) {
  const component = wrapper.findComponent({ name: componentName })
  if (!component.exists()) {
    throw new Error(`Component ${componentName} not found`)
  }
  
  await component.vm.$emit(event, payload)
  await wrapper.vm.$nextTick()
}

// Mock Element Plus message components
export function mockElMessage() {
  return {
    success: vi.fn(),
    error: vi.fn(),
    warning: vi.fn(),
    info: vi.fn()
  }
}

export function mockElMessageBox() {
  return {
    confirm: vi.fn().mockResolvedValue('confirm'),
    alert: vi.fn().mockResolvedValue('confirm'),
    prompt: vi.fn().mockResolvedValue({ value: 'test' })
  }
}

// Form testing helpers
export async function fillForm(wrapper, formData) {
  for (const [field, value] of Object.entries(formData)) {
    const input = wrapper.find(`[data-testid="${field}"] input`)
    if (input.exists()) {
      await input.setValue(value)
    }
  }
}

export async function submitForm(wrapper, formTestId = 'form') {
  const form = wrapper.find(`[data-testid="${formTestId}"]`)
  if (form.exists()) {
    await form.trigger('submit')
  }
}

// Table testing helpers
export function getTableRows(wrapper) {
  return wrapper.findAll('.el-table__row')
}

export function getTableCell(wrapper, row, column) {
  const rows = getTableRows(wrapper)
  if (rows[row]) {
    const cells = rows[row].findAll('.el-table__cell')
    return cells[column]
  }
  return null
}

export async function sortTable(wrapper, columnIndex) {
  const headers = wrapper.findAll('.el-table__header-wrapper th')
  if (headers[columnIndex]) {
    const sortButton = headers[columnIndex].find('.caret-wrapper')
    if (sortButton.exists()) {
      await sortButton.trigger('click')
    }
  }
}

Using Test Utilities ​

javascript
// tests/components/UserTable.test.js
import { describe, it, expect } from 'vitest'
import UserTable from '@/components/UserTable.vue'
import {
  createWrapper,
  getTableRows,
  getTableCell,
  sortTable,
  triggerElEvent
} from '../utils/test-utils'

const mockUsers = [
  { id: 1, name: 'John Doe', email: 'john@example.com', age: 30 },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 25 },
  { id: 3, name: 'Bob Johnson', email: 'bob@example.com', age: 35 }
]

describe('UserTable', () => {
  it('renders user data correctly', () => {
    const wrapper = createWrapper(UserTable, {
      props: { users: mockUsers }
    })
    
    const rows = getTableRows(wrapper)
    expect(rows).toHaveLength(3)
    
    // Check first row data
    expect(getTableCell(wrapper, 0, 0).text()).toBe('John Doe')
    expect(getTableCell(wrapper, 0, 1).text()).toBe('john@example.com')
    expect(getTableCell(wrapper, 0, 2).text()).toBe('30')
  })
  
  it('sorts table by name column', async () => {
    const wrapper = createWrapper(UserTable, {
      props: { users: mockUsers }
    })
    
    // Sort by name column (index 0)
    await sortTable(wrapper, 0)
    
    // Check if sorting event was emitted
    expect(wrapper.emitted('sort-change')).toBeTruthy()
  })
  
  it('emits edit event when edit button is clicked', async () => {
    const wrapper = createWrapper(UserTable, {
      props: { users: mockUsers }
    })
    
    const editButton = wrapper.find('[data-testid="edit-user-1"]')
    await editButton.trigger('click')
    
    expect(wrapper.emitted('edit-user')).toBeTruthy()
    expect(wrapper.emitted('edit-user')[0]).toEqual([mockUsers[0]])
  })
})

Best Practices ​

1. Test Structure ​

  • Use descriptive test names
  • Group related tests with describe blocks
  • Follow AAA pattern (Arrange, Act, Assert)
  • Keep tests focused and isolated

2. Component Testing ​

  • Test component behavior, not implementation
  • Use data-testid attributes for reliable element selection
  • Test user interactions and edge cases
  • Mock external dependencies

3. Async Testing ​

  • Use await and flushPromises() for async operations
  • Test loading states and error handling
  • Mock API calls and timers
  • Test timeout scenarios

4. Form Testing ​

  • Test validation rules thoroughly
  • Test form submission and reset
  • Test field interactions and dependencies
  • Test accessibility features

5. Performance ​

  • Use shallow mounting when possible
  • Mock heavy components and operations
  • Clean up after tests
  • Use test utilities for common operations

By following these testing patterns and best practices, you can ensure your Element Plus applications are well-tested, reliable, and maintainable.

Element Plus Study Guide