You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 05:24:08