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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 17:25:38