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
andflushPromises()
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.