笔者的公司对单元测试覆盖率比较高,要求写的代码必须写单元测试
最近遇到一个需求,用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引用,经过调试发现容器的 clientHeight
和 scrollTop
一直是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");
});
再运行测试就正常通过了