Vue+Vitest+Testing-Library测试重置:解决多用例重复渲染问题
问题
使用Vue、Testing-Library与Vitest组合测试含vee-validate的登录组件时,单测试用例运行正常,多测试用例同时运行时出现报错:
TestingLibraryElementError: Found multiple elements with the role "button" and name
/sign in/i
TheLogin.vue
<template> <div class="container mx-auto px-4 h-full"> <div class="flex content-center items-center justify-center h-full"> <div class="w-full lg:w-4/12 px-4"> <VeeForm v-slot="{ handleSubmit, errors, isSubmitting }" :validation-schema="schema" as="div" > <form @submit="handleSubmit($event, onSubmit)" method="post"> <div class="relative flex flex-col min-w-0 break-words w-full mb-6 shadow-lg rounded-lg bg-gray-300 border-0" > <div class="rounded-t mb-0 px-6 py-6"> <div class="text-center mb-3"> <h6 class="text-gray-600 text-sm font-bold">Sign in with</h6> </div> </div> <div class="flex-auto px-4 lg:px-10 py-10 pt-0"> <div v-if="loginError" class="text-red-500">{{loginError}}</div> <div class="text-gray-500 text-center mb-3 font-bold"> <small>Or sign in with credentials</small> </div> <div class="relative w-full mb-3"> <label class="block uppercase text-gray-700 text-xs font-bold mb-2" for="email" >Email</label > <Field id="email" name="email" placeholder="email" class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full" style="transition: all 0.15s ease 0s" :disabled="isSubmitting" :class="{ 'border-red-500': errors.email }" /> <ErrorMessage class="text-red-500 text-xs" name="email" /> </div> <div class="relative w-full mb-3"> <label class="block uppercase text-gray-700 text-xs font-bold mb-2" for="password" >Password</label > <Field id="password" name="password" type="password" class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full" placeholder="Password" style="transition: all 0.15s ease 0s" :disabled="isSubmitting" :class="{ 'border-red-500': errors.password }" /> <ErrorMessage class="text-red-500 text-xs" name="password" /> </div> <div> <label class="inline-flex items-center cursor-pointer" ><input id="customCheckLogin" type="checkbox" class="form-checkbox border-0 rounded text-gray-800 ml-1 w-5 h-5" style="transition: all 0.15s ease 0s" /><span class="ml-2 text-sm font-semibold text-gray-700" >Remember me</span ></label > </div> <div class="text-center mt-6"> <button class="bg-gray-900 text-white active:bg-gray-700 text-sm font-bold uppercase px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 w-full" type="submit" style="transition: all 0.15s ease 0s" :disabled="isSubmitting" v-text="isSubmitting ? 'Processing' : 'Sign In'" ></button> </div> </div> </div> </form> </VeeForm> </div> </div> </div> </template> <script lang="ts" setup> import axios from "axios"; import { ErrorMessage, Form as VeeForm, Field } from "vee-validate"; import { ref } from "vue"; import * as yup from "yup" import { useRouter } from 'vue-router' const loading = ref(false) const loginError =ref('') const router = useRouter() const schema = yup.object({ email: yup.string().required().min(8).label("Email"), password: yup.string().required().min(8).label("Password") }); const onSubmit = async (values, actions) => { loading.value = true; const formData = { email: values.email, password: values.password }; axios .post("backend-url", formData) .then((response) => { loading.value = false; router.push({ name: "products.index" }); }) .catch((error) => { actions.setErrors(error.response.data.errors); loginError.value = error.message; }); }; </script>
测试代码
import { describe, it, expect, beforeEach, test } from 'vitest' import TheLogin from '@/Pages/TheLogin.vue' import flushPromises from 'flush-promises'; import waitForExpect from 'wait-for-expect'; import {fireEvent, render, screen} from '@testing-library/vue' describe('TheLogin', () => { test('renders error message if email is empty', async () => { const {getAllByRole, getByRole, findByRole } = render(TheLogin); const button = getByRole('button', { name: /sign in/i }) await fireEvent.click(button) await flushPromises() await waitForExpect(() => { const errorElement = getAllByRole('alert') //this is better because the error message could change expect(errorElement[0].textContent).toBeTruthy() //this also works //getByText('Email is a required field') }) }) test('renders error message if password is empty', async () => { const {getAllByRole, getByPlaceholderText, getByRole } = render(TheLogin) const button = getByRole('button', { name: /sign in/i }) const emailInput = getByPlaceholderText(/email/i) await fireEvent.update(emailInput, 'test@gmail.com') await fireEvent.click(button) await flushPromises() await waitForExpect(() => { const errorElement = getAllByRole('alert') expect(errorElement[0].textContent).toBeTruthy() }) }) })
解决方案
问题根源是测试用例未正确隔离,组件实例在多个测试中残留,导致DOM中存在多个相同组件。可以通过以下方式解决:
1. 自动清理DOM元素
在测试文件顶部添加afterEach钩子,调用Testing Library的cleanup方法,确保每个测试结束后移除所有渲染的DOM元素:
import { cleanup } from '@testing-library/vue' afterEach(() => { cleanup() })
2. 改用screen对象查询(推荐)
避免解构render返回的查询方法,直接使用screen对象的查询方法,它会自动作用于当前测试的DOM范围,配合cleanup确保查询不会跨测试污染:
修改后的测试代码:
test('renders error message if email is empty', async () => { render(TheLogin); const button = screen.getByRole('button', { name: /sign in/i }) await fireEvent.click(button) await flushPromises() await waitForExpect(() => { const errorElement = screen.getAllByRole('alert') expect(errorElement[0].textContent).toBeTruthy() }) }) test('renders error message if password is empty', async () => { render(TheLogin) const button = screen.getByRole('button', { name: /sign in/i }) const emailInput = screen.getByPlaceholderText(/email/i) await fireEvent.update(emailInput, 'test@gmail.com') await fireEvent.click(button) await flushPromises() await waitForExpect(() => { const errorElement = screen.getAllByRole('alert') expect(errorElement[0].textContent).toBeTruthy() }) })
3. 重置vee-validate状态(可选)
如果上述方法无效,可在beforeEach中重置vee-validate的表单状态,避免全局状态残留:
import { useForm } from 'vee-validate' beforeEach(() => { const { resetForm } = useForm() resetForm() })
内容的提问来源于stack exchange,提问作者DeveloperX
相关产品推荐
相关产品推荐

