一个Vue3组件单元测试引发的思考

笔者的公司对单元测试覆盖率比较高,要求写的代码必须写单元测试

最近遇到一个需求,用Vue3实现固定高度的虚拟列表组件,由于组件中使用到某些属性导致写单元测试时一直遇到问题,经过笔者耐心调试后,找到导致问题的原因并给出解决方法,由此记录在本篇文章中

固定高度的虚拟列表组件

固定高度的虚拟列表组件是指在前端开发中,展示大量数据时的一种优化方案,旨在通过"虚拟化"技术来提高性能,尤其是在处理大量数据时,减少渲染和 DOM 节点数量,从而提高页面的渲染速度和用户体验。

请看笔者写的示例代码

vue 复制代码
<script setup>
import { ref, onMounted, watch, defineProps } from "vue";

const props = defineProps({
  itemHeight: {
    type: Number,
    default: 50,
  },
  items: {
    type: Array,
  },
});

const listContainer = ref(null);
const visibleItems = ref([]);
const totalHeight = ref(0);
const startIndex = ref(0);
const endIndex = ref(0);
const containerHeight = ref(0);

// 计算总高度
const getTotalHeight = () => {
  return props.items.length * props.itemHeight;
};

// 获取每个列表项的样式
const getItemStyle = (index) => {
  return {
    position: "absolute",
    top: `${index * props.itemHeight}px`,
    height: `${props.itemHeight}px`,
    width: "100%",
  };
};

// 更新可见项
const updateVisibleItems = () => {
  const container = listContainer.value;

  const scrollTop = container.scrollTop;
  const visibleCount = Math.ceil(container.clientHeight / props.itemHeight);

  const start = Math.max(0, Math.floor(scrollTop / props.itemHeight));
  const end = Math.min(
    props.items.length - 1,
    Math.ceil((scrollTop + container.clientHeight) / props.itemHeight)
  );

  visibleItems.value = props.items.slice(start, end + 1);

  startIndex.value = start;
  endIndex.value = end;
};

// 滚动事件处理
const onScroll = () => {
  updateVisibleItems();
};

watch(
  () => listContainer.value?.clientHeight,
  () => {
    containerHeight.value = listContainer.value.clientHeight;
    totalHeight.value = getTotalHeight();
    updateVisibleItems();
  }
);

// 监听 items 的变化
watch(
  () => props.items,
  () => {
    updateVisibleItems();
  }
);
</script>

<template>
  <div
    class="virtual-list"
    ref="listContainer"
    @scroll="onScroll"
    data-testid="virtual-list"
  >
    <div class="list" :style="{ height: totalHeight + 'px' }">
      <div
        v-for="(item, index) in visibleItems"
        :key="item.id"
        :style="getItemStyle(startIndex + index)"
        class="list-item"
        data-testid="virtual-list-item"
      >
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<style scoped>
.virtual-list {
  position: relative;
  overflow-y: auto;
  border: 1px solid #ccc;
  width: 300px;
  height: 400px;
}

.list {
  position: relative;
  width: 100%;
}

.list-item {
  position: absolute;
  width: 100%;
}

.item {
  padding: 10px;
  border-bottom: 1px solid #ccc;
}
</style>

写完组件后,笔者就去写该组件的单元测试了,单元测试也很快写好了,请看下面的示例代码

js 复制代码
it("render list item correctly", async () => {
    const wrapper = mount(VirtualList, {
      props: {
        items,
        itemHeight: 50,
      },
      slots: {
        default: ({ item }) => `<div>${item.name}</div>`,
      },
    });
    
    await wrapper.vm.$nextTick();

    expect(wrapper.text()).toContain("Item 1");
    expect(wrapper.text()).toContain("Item 2");
  });

可是运行时直接报错了,错误信息如下所示

错误信息显示只有Item1没有Item2, 太奇怪了。最起码会渲染很多Item,绝不是一个Item。然后笔者耐心调试

调试

来看看组件中的几处代码

js 复制代码
 const container = listContainer.value;

  const scrollTop = container.scrollTop;
  const visibleCount = Math.ceil(container.clientHeight / props.itemHeight);

  const start = Math.max(0, Math.floor(scrollTop / props.itemHeight));
  const end = Math.min(
    props.items.length - 1,
    Math.ceil((scrollTop + container.clientHeight) / props.itemHeight)
  );

  visibleItems.value = props.items.slice(start, end + 1);

  startIndex.value = start;
  endIndex.value = end;

container 是虚拟列表容器的DOM引用,经过调试发现容器的 clientHeightscrollTop一直是0,没有取到实际的值

一开始笔者以为是@vue/test-utils导致的,当笔者换了@testing-library/vue后,依旧报错,笔者就感觉不是测试框架的问题

经过一番排查,发现了如下有用的信息

根本原因是jsdom不支持,导致测试环境中无法提供一些DOM节点属性,造成测试无法正常执行

解决方案

如何解决jsdom带来的这个问题呢?可以通过mock需要使用的属性,方法如下

js 复制代码
  it("render list item correctly", async () => {
    const wrapper = mount(VirtualList, {
      props: {
        items,
        itemHeight: 50,
      },
      slots: {
        default: ({ item }) => `<div>${item.name}</div>`,
      },
    });

    // 解决方案如下
    const clientHeightMock = 200;

    const listContainer = wrapper.find(".virtual-list").element;
    Object.defineProperty(listContainer, "clientHeight", {
      value: clientHeightMock,
    });

    await wrapper.vm.$nextTick();

    expect(wrapper.text()).toContain("Item 1");
    expect(wrapper.text()).toContain("Item 2");
    expect(wrapper.text()).toContain("Item 3");
    expect(wrapper.text()).toContain("Item 4");
  });

再运行测试就正常通过了

相关推荐
m0_7482487717 分钟前
YOLOv5部署到web端(flask+js简单易懂)
前端·yolo·flask
qwaesrdt320223 分钟前
【如何使用大语言模型(LLMs)高效总结多文档内容】
前端
Ace_31750887761 小时前
淘宝平台通过关键字搜索获取商品列表技术贴
前端
卸任1 小时前
国产 Dev/Ops 工具 Jpom 的前端项目自动化部署实践
运维·前端
一个处女座的程序猿O(∩_∩)O1 小时前
vue 如何实现复制和粘贴操作
前端·javascript·vue.js
赔罪1 小时前
HTML-列表标签
服务器·前端·javascript·vscode·html·webstorm
谦谦橘子1 小时前
手写React useEffect方法,理解useEffect原理
前端·javascript·react.js
九州~空城2 小时前
C++中map和set的封装
java·前端·c++
椒盐大肥猫2 小时前
axios拦截器底层实现原理
前端·javascript
夕水2 小时前
我的2024-人生须为有益事
前端·年终总结