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

Vue组件测试问题:无法获取Day组件的有效date props

Vue组件测试问题:无法获取fillArray生成的日期数组

我在为Vue组件Calendar.vue编写测试时,需要获取fillArray()函数生成的日期数组。尝试通过读取Day.vue组件的props来实现,但得到的数组全是null。推测fillArray函数在测试执行时未运行,试过flushPromises等方法也无法解决。

Calendar.vue组件代码

<script setup lang="ts">
import Day from '@/components/Day.vue'
import { onMounted, ref } from 'vue'
import Weekdays from '@/components/Weekdays.vue'
import MonthSelector from '@/components/MonthSelector.vue'
import { useDateStore } from '@/helpers/date'
import CalendarHeader from '@/components/CalendarHeader.vue'

const dateArray = ref<(Date)[]>([]);
const selectedYear = ref(new Date().getFullYear());
const selectedMonth = ref(new Date().getMonth() + 1);
const dateStore = useDateStore();

const numberOfDays = (year: number, month: number) => new Date(year, month, 0).getDate();
const getWeekday = (year: number, month: number, day: number) => new Date(year, month - 1, day).getDay();

const fillArray = () => {
  const daysInMonth = numberOfDays(selectedYear.value, selectedMonth.value);
  const firstWeekday = getWeekday(selectedYear.value, selectedMonth.value, 1);

  const totalDays = firstWeekday + daysInMonth;
  dateArray.value = new Array(totalDays).fill(null);

  let currentDay = 1;

  for (let i = firstWeekday; i < firstWeekday + daysInMonth; i++) {
    dateArray.value[i] = new Date(selectedYear.value, selectedMonth.value - 1, currentDay);
    currentDay++;
  }

  const previousMonthDays = numberOfDays(selectedYear.value, selectedMonth.value - 1);
  for (let i = firstWeekday - 1; i >= 0; i--) {
    dateArray.value[i] = new Date(selectedYear.value, selectedMonth.value - 2, previousMonthDays - (firstWeekday - 1 - i));
  }
};

const navigateForwards = () => {
  if (selectedMonth.value === 12) {
    selectedMonth.value = 1;
    selectedYear.value++;
  } else {
    selectedMonth.value++;
  }
  fillArray();
};

const navigateBackwards = () => {
  if (selectedMonth.value === 1) {
    selectedMonth.value = 12;
    selectedYear.value--;
  } else {
    selectedMonth.value--;
  }
  fillArray();
};


onMounted(() => {
  fillArray();
  dateStore.initialize();
});
</script>

<template>
  <div class="calendar">
    <div class="calendar__table">
      <CalendarHeader/>
      <MonthSelector :month="selectedMonth" :year="selectedYear" @navigateForwards="navigateForwards" @navigateBackwards="navigateBackwards"/>
      <Weekdays/>
      <div class="calendar__row">
        <Day
          v-for="(date, index) in Array.from({ length: 7 }, (_, i) => dateArray[i] || null)"
          :key="index"
          :date="date"
          :ill="false"
          :vacation="false"
        />
      </div>

      <div class="calendar__row">
        <Day
          v-for="(date, index) in Array.from({ length: 7 }, (_, i) => dateArray[i + 7] || null)"
          :key="index + 7"
          :date="date"
          :ill="false"
          :vacation="false"
        />
      </div>

      <div class="calendar__row">
        <Day
          v-for="(date, index) in Array.from({ length: 7 }, (_, i) => dateArray[i + 14] || null)"
          :key="index + 14"
          :date="date"
          :ill="false"
          :vacation="false"
        />
      </div>

      <div class="calendar__row">
        <Day
          v-for="(date, index) in Array.from({ length: 7 }, (_, i) => dateArray[i + 21] || null)"
          :key="index + 21"
          :date="date"
          :ill="false"
          :vacation="false"
        />
      </div>

      <div class="calendar__row">
        <Day
          v-for="(date, index) in Array.from({ length: 7 }, (_, i) => dateArray[i + 28] || null)"
          :key="index + 28"
          :date="date"
          :ill="false"
          :vacation="false"
        />
      </div>
    </div>
  </div>
</template>

测试代码

import { flushPromises, mount, shallowMount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import Calendar from '@/components/Calendar.vue';
import { createPinia, setActivePinia } from 'pinia';
import MonthSelector from '@/components/MonthSelector.vue';
import { defineComponent } from 'vue';


vi.mock('@/helpers/date', () => ({
  useDateStore: vi.fn(() => ({
    initialize: vi.fn()
  }))
}));

describe('Calendar.vue', () => {
  let wrapper: any;

  const MockMonthSelector = defineComponent({
    template: `<div></div>`,
    props: {
      month: Number,
      year: Number,
    },
    methods: {
      navigateForwards() {
        this.$emit('navigateForwards');
      },
      navigateBackwards() {
        this.$emit('navigateBackwards');
      },
    },
  });

  const shallowMountCut = () => {
    const cut = mount(Calendar,{
      global:{
        components:{
          MockMonthSelector
        }
      }
    });

    return {
      getSelectedYear: () => cut.findComponent(MonthSelector).props('year'),
      setSelectedMonth: (number: number) => (cut.vm as any).selectedMonth = number,
      navigateBackwards: () => cut.findComponent(MockMonthSelector),
      findNavigateForwards: () => cut.findComponent(MockMonthSelector),
      findCalendarRow: () => cut.find('.calendar__row'),
      getDateArray: () => cut.findAllComponents({ name: 'Day' }).map((day) => day.props().date?.getDate() || null),
    };
  };

  beforeEach(() => {
    wrapper = shallowMount(Calendar);
    setActivePinia(createPinia());

    wrapper.vm.$nextTick;
  });

  it('should have correct amount of days', async () => {
    await flushPromises();
    const { getDateArray } = shallowMountCut()
    expect(getDateArray()).toHaveLength(35);
    console.log(getDateArray())
  });
});

当前测试结果

[
  null, null, null, null, null,
  null, null, null, null, null,
  null, null, null, null, null,
  null, null, null, null, null,
  null, null, null, null, null,
  null, null, null, null, null,
  null, null, null, null, null
]

我期望数组中包含有效的Date对象,后续测试需要用到这些值。

Day.vue代码

<script setup>
import { computed, ref } from 'vue'
import { useDateStore } from '@/helpers/date'

const props = defineProps({
  date: Date,
  ill: Boolean,
  vacation: Boolean,
});

const store = useDateStore();
let day = ref<string>();

const convertDate = () => {
  if (props.date != undefined) {
    day.value = props.date.getDate().toString();
  }
  return day.value;
}
const boxClass = computed(() => {
  if (props.ill) {
    return 'day-box__ill';
  } else if (props.vacation) {
    return 'day-box__vacation';
  } else {
    return 'day-box__normal';
  }
});

const isSelected = computed(() => {
  if (store.selectedDate == props.date) {
    return 'day-box__selected';
  } else {
    return '';
  }
});

const valueSupplied = () => {
  return props.date != undefined;
}

const clicked = () => {
  if (props.date != undefined) {
    store.selectedDate = (props.date);
  }
}
</script>

<template>
  <div v-if="valueSupplied()" :class="['day-box', boxClass, isSelected]" @click="clicked">
    <b class="day-box__text">{{ convertDate() }}</b>
  </div>
  <div v-else class="day-box"/>
</template>

问题修复方案

关键问题点

  1. Pinia初始化顺序错误:组件挂载前必须先创建并激活Pinia,否则组件内的useDateStore无法获取实例,可能阻塞onMounted钩子执行。
  2. Mock组件注册名不匹配:原组件使用<MonthSelector>,但测试中注册Mock组件时用了MockMonthSelector作为键名,导致原组件未被替换,渲染异常。
  3. 重复创建组件实例:测试用例中调用shallowMountCut()会生成新的组件实例,未等待其生命周期完成就获取数据;同时beforeEach中的实例未被复用。
  4. 未等待DOM更新:$nextTick仅被引用未调用,未等待fillArray执行完毕和视图更新。

修复后的测试代码

import { flushPromises, mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import Calendar from '@/components/Calendar.vue';
import { createPinia, setActivePinia } from 'pinia';
import { defineComponent } from 'vue';

vi.mock('@/helpers/date', () => ({
  useDateStore: vi.fn(() => ({
    initialize: vi.fn()
  }))
}));

describe('Calendar.vue', () => {
  let wrapper: any;
  let cutHelpers: any;

  const MockMonthSelector = defineComponent({
    template: `<div></div>`,
    props: {
      month: Number,
      year: Number,
    },
    methods: {
      navigateForwards() {
        this.$emit('navigateForwards');
      },
      navigateBackwards() {
        this.$emit('navigateBackwards');
      },
    },
  });

  beforeEach(async () => {
    // 先初始化Pinia再挂载组件
    setActivePinia(createPinia());
    wrapper = mount(Calendar, {
      global: {
        components: {
          // 注册名与原组件一致
          MonthSelector: MockMonthSelector
        }
      }
    });
    // 等待onMounted执行和DOM更新
    await flushPromises();
    await wrapper.vm.$nextTick();
    cutHelpers = {
      getDateArray: () => wrapper.findAllComponents({ name: 'Day' }).map((day) => day.props().date?.getDate() || null),
    };
  });

  it('should have correct amount of days and valid dates', async () => {
    const dateArray = cutHelpers.getDateArray();
    expect(dateArray).toHaveLength(35);
    // 验证至少有部分非null值
    expect(dateArray.some(val => val !== null)).toBe(true);
    console.log(dateArray);
  });
});

修复说明

  1. 调整Pinia初始化顺序:在beforeEach中先调用setActivePinia(createPinia()),再挂载组件,确保组件能获取到Pinia实例。
  2. 修正Mock组件注册名:将MockMonthSelector注册为MonthSelector,与原组件名匹配,确保组件正确渲染。
  3. 复用组件实例:在beforeEach中挂载一次组件并等待生命周期完成,测试用例直接复用该实例的辅助方法,避免重复创建。
  4. 等待DOM更新:在挂载后调用await flushPromises()和await wrapper.vm.$nextTick(),确保fillArray执行完毕且视图更新完成后再获取数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 07:17:31