Vitest挂载Vue组件时报TypeError: Cannot set property 'id' of undefined
问题:Vitest挂载Vue组件时抛出TypeError: Cannot set property 'id' of undefined
- 错误现象:使用Vitest挂载组件测试时,无论挂载Login还是其他组件,都会抛出
TypeError: Cannot set property 'id' of undefined - 排查过程:最初怀疑是Login组件第160行
res.data.data.id导致,但注释该行后错误依然存在
测试代码
import { mount } from "@vue/test-utils"; import { describe, expect, test, it } from "vitest"; import Login from "../views/Login.vue"; import OrderItemsList from "../components/orders/OrderItemsList.vue"; describe("Login Test", async () => { test("Verifica che le credenziali di input rispettino i criteri", async () => { const validateInput = (s: String = ""): boolean => s.length > 0; const username = "*****"; const password = "*****"; expect(validateInput(username)).toBe(true); expect(validateInput(password)).toBe(true); const wrapper = mount(Login); // <--------------- /* wrapper.get('[name="serverIpAddress"]').setValue("localhost"); //wrapper.setData({ username: ' '.repeat(7) }) wrapper.get('[name="serverIpAddress"]').trigger("keyup.enter"); //wrapper.get('[id="serverButton"]').trigger("click"); wrapper.get('[name="username"]').setValue(username); wrapper.get('[name="password"]').setValue(password); wrapper.get('[name="password"]').trigger("keyup.enter"); //expect(typeof localStorage.getItem("currentUserId")).toEqual("number"); //*/ }); });
Login组件代码
<template> <v-main> <v-container fluid fill-height> <v-layout align-center justify-center> <v-flex xs12 sm8 md5> <v-card class="elevation-12"> <v-toolbar dark color="primary"> <v-toolbar-title>Entra in Velocissimo</v-toolbar-title> </v-toolbar> <v-stepper v-model="stepper" vertical> <v-stepper-step :complete="stepper > 1" step="1"> Server<span v-html="serverHostText"></span> </v-stepper-step> <v-stepper-content step="1" class="text-left"> <br /> <v-btn elevation="2" v-on:click="useAsServer()" >Attiva server </v-btn> <br /> <br /> <br /> oppure inserisci l'indirizzo ip del server <br /> <br /> <v-text-field v-model.trim="serverIpAddress" name="serverIpAddress" label="Indirizzo IP" placeholder="192.168.1.X" v-on:keyup.enter="goTo(2)" autofocus ></v-text-field> <br /> <v-btn color="primary" @click="goTo(2)"> Avanti </v-btn> </v-stepper-content> <v-stepper-step :complete="stepper > 2" step="2"> Login </v-stepper-step> <v-stepper-content step="2" class="text-left"> <form ref="form" @submit.prevent="login()"> <v-text-field ref="usernameInput" v-model.trim="username" name="username" label="Username" type="text" placeholder="username" prepend-icon="mdi-account" required :disabled="loading" autocomplete="username" ></v-text-field> <v-text-field ref="passwordInput" v-model.trim="password" name="password" label="Password" type="password" placeholder="password" prepend-icon="mdi-key" autocomplete="current-password" required :disabled="loading" v-on:keyup.enter="login()" ></v-text-field> <div class="red--text">{{ errorMessage }}</div> </form> <v-btn :loading="loading" :disabled="loading" color="primary" @click="login()" > Accedi </v-btn> <v-btn text @click="backToServer()" :disabled="loading"> Indietro </v-btn> </v-stepper-content> </v-stepper> </v-card> </v-flex> </v-layout> </v-container> </v-main> </template> <script lang="ts"> import { Component, Ref, Vue } from "vue-property-decorator"; import { LoginRequest } from "@/db-webserver/pluto-api/RequestResponse"; import { LocalDBWSApi, MainDBWSApi } from "@/modules/dbws-api"; import { wrap } from "@/db-webserver/starter/WebserverHelper"; import { AxiosResponse } from "axios"; import router from "@/router"; import { DBWSManager } from "@/modules/dbws-manager/electron"; import { LocalConfiguration } from "@/modules/local-configuration"; import { Auth } from "@/modules/auth"; import { routeName } from "@/router/types"; import { ApiResponse } from "@/modules/dbws-api/DBWSApiClass"; @Component export default class Login extends Vue { @Ref("usernameInput") usernameInput: HTMLInputElement; @Ref("passwordInput") passwordInput: HTMLInputElement; private stepper = 1; private serverIpAddress: string = ""; private username = ""; private password = ""; private errorMessage = ""; private isUseAsServer = false; private loading = false; public async login(): Promise<void> { this.loading = true; this.blurAllInputs(); const ipAddress = this.isUseAsServer ? "localhost" : this.serverIpAddress; await LocalConfiguration.setDBWSHost(ipAddress); await MainDBWSApi.setup(ipAddress); await DBWSManager.start(); const credentials: LoginRequest = { email: this.username, password: this.password, }; await wrap( async () => { const res = await MainDBWSApi.login(credentials); await Auth.login({ plutoToken: res.data.data.token }); if (this.isUseAsServer) { await DBWSManager.configurePlutoInstance(res.data.data); await DBWSManager.syncAllData(); } else { await DBWSManager.configurePlutoInstance(res.data.data, LocalDBWSApi); await DBWSManager.syncPrintData(LocalDBWSApi); } await DBWSManager.enableContinuousSync(); await LocalConfiguration.setIsDBWSServer(this.isUseAsServer); await LocalConfiguration.setCurrentUserId(res.data.data.id); await router.push({ name: routeName.ORDERS_LIST }); }, { errorWithResponse: (res: AxiosResponse<ApiResponse<any>>) => { this.errorMessage = "Errore: " + res.data.message; }, serviceUnavailable: () => { this.errorMessage = "Errore: controlla la connessione al server locale"; }, } ); this.loading = false; } public goTo(step: number) { this.stepper = step; if (step == 2) { this.errorMessage = ""; this.usernameInput.focus(); } } public blurAllInputs() { this.usernameInput.blur(); this.passwordInput.blur(); } public useAsServer() { this.isUseAsServer = true; this.goTo(2); this.serverIpAddress = "Questo dispositivo"; } public backToServer() { if (this.isUseAsServer) { this.serverIpAddress = ""; } this.isUseAsServer = false; this.goTo(1); } get serverHostText(): string { if (this.stepper === 1) { return ""; } let text: string; if (this.isUseAsServer) { text = "Questo dispositivo"; } else { text = this.serverIpAddress; } return `: <i>${text}</i>`; } } </script>
依赖版本
{ "@vitejs/plugin-vue": "^3.2.0", "vitest": "^0.25.8", "@babel/preset-typescript": "^7.18.6" }
解决方案
1. 锁定问题根源
挂载任意组件都报错,说明问题不在组件本身,而是测试环境的全局依赖未正确Mock或初始化。比如LocalConfiguration、DBWSManager这类模块在测试环境中可能返回undefined,导致后续设置id时抛出错误。
2. Mock第三方模块
在测试文件开头对所有全局依赖进行Mock,避免实际调用外部服务或未初始化的模块:
// 测试文件顶部添加Mock代码 vi.mock('@/modules/local-configuration', () => ({ LocalConfiguration: { setDBWSHost: vi.fn(), setIsDBWSServer: vi.fn(), setCurrentUserId: vi.fn() } })); vi.mock('@/modules/dbws-manager/electron', () => ({ DBWSManager: { start: vi.fn(), configurePlutoInstance: vi.fn(), syncAllData: vi.fn(), syncPrintData: vi.fn(), enableContinuousSync: vi.fn() } })); vi.mock('@/modules/dbws-api', () => ({ MainDBWSApi: { setup: vi.fn(), login: vi.fn().mockResolvedValue({ data: { data: { id: 1, token: 'mock-token' } } }) }, LocalDBWSApi: {} })); vi.mock('@/modules/auth', () => ({ Auth: { login: vi.fn() } })); vi.mock('@/router', () => ({ default: { push: vi.fn() } }));
3. 修复组件DOM引用问题
组件中goTo(2)会调用usernameInput.focus(),但挂载组件时初始stepper为1,若触发goTo(2)时DOM未完全渲染,会导致usernameInput为undefined。修改组件逻辑添加安全判断:
// 修改Login组件的goTo方法 public goTo(step: number) { this.stepper = step; if (step == 2) { this.errorMessage = ""; this.$nextTick(() => { this.usernameInput?.focus(); // 添加可选链避免undefined报错 }); } }
4. 检查测试环境配置
确认vitest.config.ts正确配置Vue支持:
import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true } })
内容的提问来源于stack exchange,提问作者Bilox
相关产品推荐
相关产品推荐

