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

Vue Test Utils v2:测试keydown事件下v-model值更新报错排查

Vue Test Utils + Vitest 测试v-model更新时遇$emit报错问题

我是Vue Test Utils新手,用Vite搭配Vitest做测试,在测试自定义AutoComplete组件的v-model更新逻辑时卡壳了。这个组件有input和results两个插槽,input插槽监听@keydown事件:按下特殊按键会触发对应操作,普通按键则要更新输入框值和对应的modelValue。

简化后的AutoComplete组件代码

<script setup>
import { ref, watchEffect, computed} from 'vue';

// Props
const props = defineProps({
    modelValue : {
        type: [String, null],
        required: true
    },
    autoCompleteConfigs : {
        type: Array,
        required: true,
    }
});

const emit = defineEmits(['selected','chosen','updated','update:modelValue']);

const methods = {
    // 用户选择结果时的逻辑
    onChosen :  (result) =>{
        /// 省略业务逻辑
    },

    moveSelection: (x) =>{
        /// 省略业务逻辑
    },

    // 键盘按下事件处理
    onKeydown : (event) => {
        // 省略判断逻辑
        const passThruKeys = ['ArrowUp','ArrowDown','Tab','Enter','Delete'];
        if (passThruKeys.includes(event.key)) {
            event.preventDefault();
            // 省略按键对应逻辑
        }
    }
};

// 组件内用于存储输入查询内容的ref
const query = ref('');
</script>

<template>
    <!-- input插槽 -->
    <div>
        <slot name="input" @keydown="methods.onKeydown" ></slot>
    </div>

    <!-- results插槽 -->
    <div>
        <slot v-if="searchResults.length > 0" name="results" :results="searchResults" :indexSearched="currentSearchIndex" @chosen="methods.onChosen"></slot>
    </div>
</template>

我的测试代码

import { mount} from "@vue/test-utils";
import AutoComplete from '@/Components/AutoComplete.vue'
import { describe, it, expect, vi} from "vitest";

describe("AutoComplete", () => {
    let wrapper;

    beforeEach(() => {
        wrapper = mount(AutoComplete, {
            props: {
                modelValue: 'Some text ',
                'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
                autoCompleteConfigs : [
                    {
                        trigger : '#',
                        index : 'tag',
                        autoComplete : 'tag'
                    },
                    {
                        trigger : '@',
                        index : 'user',
                        autoComplete : 'username'
                    }
                ],
            },
            slots: {
                input: `<input :value="modelValue" 
                         @keydown="onKeydown" 
                         id="message" 
                         @input="$emit(
                               'update:modelValue', 
                                $event.target.value
                         )"/>`,
            },
        });
    });

    it("should update model value", async() => {
        const input = wrapper.find('#message');
        input.setValue(wrapper.props().modelValue + "#h");
        await wrapper.vm.$nextTick();
        expect (wrapper.vm.query).toBe("h");
    });
});

运行测试后报错

TypeError: _ctx.$emit is not a function


问题原因及修复方案

问题根源

  1. 插槽模板里的$emit属于测试上下文的父级,无法直接调用AutoComplete组件的emit方法;
  2. 测试里手动绑定onUpdate:modelValue的写法不够规范;
  3. <script setup>里的query是组件私有变量,测试无法直接访问。

修复步骤

  1. 修正插槽的$emit调用:
    插槽input里的$emit改为调用组件实例的$emit,通过$parent访问(插槽内容挂载在组件内部,$parent指向AutoComplete实例)。
  2. 规范v-model事件监听:
    单独通过listeners监听update:modelValue事件,更清晰跟踪值的变化。
  3. 暴露组件内部变量:
    在AutoComplete组件里用defineExpose把query暴露出来,让测试能访问到。

修正后的测试代码

import { mount} from "@vue/test-utils";
import AutoComplete from '@/Components/AutoComplete.vue'
import { describe, it, expect} from "vitest";

describe("AutoComplete", () => {
    let wrapper;
    let updatedModelValue;

    beforeEach(() => {
        updatedModelValue = '';
        wrapper = mount(AutoComplete, {
            props: {
                modelValue: 'Some text ',
                autoCompleteConfigs : [
                    {
                        trigger : '#',
                        index : 'tag',
                        autoComplete : 'tag'
                    },
                    {
                        trigger : '@',
                        index : 'user',
                        autoComplete : 'username'
                    }
                ],
            },
            slots: {
                input: `<input :value="modelValue" 
                         @keydown="onKeydown" 
                         id="message" 
                         @input="$parent.$emit('update:modelValue', $event.target.value)"/>`,
            },
            listeners: {
                'update:modelValue': (val) => {
                    updatedModelValue = val;
                    wrapper.setProps({ modelValue: val });
                }
            }
        });
    });

    it("should update model value and query", async() => {
        const input = wrapper.find('#message');
        const newInputValue = `${wrapper.props().modelValue}#h`;
        
        await input.setValue(newInputValue);

        // 断言modelValue更新正确
        expect(updatedModelValue).toBe(newInputValue);
        // 断言组件内部query值正确(需组件暴露query)
        expect(wrapper.vm.query).toBe("h");
    });
});

组件补充修改

在AutoComplete组件的<script setup>末尾添加defineExpose:

<script setup>
// ... 原有代码

// 暴露内部变量给测试
defineExpose({ query });
</script>

内容的提问来源于stack exchange,提问作者Ben

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 00:54:53