测试 vue 组件
我们用 @vue/test-utils 来测试 vue 组件。
下载三个依赖
# 用来模拟浏览器环境
pnpm i jsdom -D
# 用来解析 vue 单文件组件
pnpm i @vitejs/plugin-vue -D
# 用来生成 vue 组件实例
pnpm i @vue/test-utils -D配置插件
在 vitest.config.ts 中:
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
/**
* 配置环境
*
* @see environment https://cn.vitest.dev/config/#environment
*
* 这里建议使用 jsdom 因为 happy-dom 会有一些不可预期错误,详情参考:
*
* @see test-utils https://github.com/vuejs/test-utils/issues/1704
* @see test-utils https://github.com/vuejs/test-utils/issues/1602
* @see fighting-design https://github.com/FightingDesign/fighting-design/pull/346
*/
environment: 'jsdom',
},
})断定组件的根标签包含有某个类名
vue 组件:
<template>
<div class="text-red">
<div class="ma-10">asdf</div>
</div>
</template>mount 方法用来挂载 vue 单文件组件,并返回一个 wrapper 对象。wrapper 对象上有一些方法和属性。
wrapper.classes('text-red') 传入某个具体的类名,会判断该类名是否存在,返回布尔值。
wrapper.classes() 方法传入具体的类型,返回所有由类名组成的数组。
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', () => {
const wrapper = mount(testVueComp)
expect(wrapper.classes('text-red')).toBe(true)
expect(wrapper.classes()).toContain('text-red')
})获取组件的某个节点,并断定它还有其它类名
vue 模板:
<template>
<div class="text-red">
<div class="abc def">asdf</div>
</div>
</template>wrapper.get 方法返回匹配选择器的第一个 dom 节点或 vue 组件的 wrapper。
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', () => {
const wrapper = mount(testVueComp)
expect(wrapper.get('.abc').classes('def')).toBe(true)
})给组件传入 prop
const wrapper = mount(testVueComp, {
props: {
nickName: '张三'
}
})断言标签内的文本内容
vue 组件:
<template>
<div class="name">
{{ nickName }}
</div>
</template>
<script>
export default {
props: {
nickName: ''
}
}
</script>使用 wrapper.get('.name').text() 方法可以获得匹配选择器标签内部的文本内容。
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', () => {
const wrapper = mount(testVueComp, {
props: {
nickName: '张三'
}
})
expect(wrapper.get('.name').text()).toBe('张三')
})获取某个节点的 attributes 属性对象
vue 组件:
<template>
<div :info="info" :isShow="isShow" b c>
</div>
</template>
<script>
export default {
props: ['info'],
data() {
return {
isShow: "true"
}
}
}
</script>使用 wrapper.attributes() 方法,可以获取到匹配选择器的所有 attributes。注意不包括父组件传入进来的 prop 属性,以及组件的 data 数据。
这里注意,虽然打印出的 attributes() 包含了 data 中的 isShow。但是如果打印 attributes('isShow'),结果为 undefined。如果想要断言 prop、data,可以从 wrapper.vm 对象上获取并进行断言。
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', () => {
const wrapper = mount(testVueComp, {
info: '123'
})
console.log(wrapper.attributes())
expect(wrapper.attributes())
})高亮代码行的打印部分如下。可以看到并不包含 prop。
也由此知道,如果省略给 attribute 赋值,那么 attribute 默认为空串。
{ isshow: 'true', b: '', c: '' }断言节点存在某个属性
使用 wrapper.attribute('.selector') 方法。
<template>
<div b c>
</div>
</template>如果省略给 attribute 赋值,那么 attribute 默认为空串。
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', () => {
const wrapper = mount(testVueComp)
expect(wrapper.attributes('b')).toBe('')
})点击某个元素后,断言某个响应式数据会变化
组件:
<template>
<div class="button" @click="add">
{{ msg }}
</div>
</template>
<script>
export default {
data() {
return {
msg: ''
}
},
methods: {
add() {
this.msg = 123
}
}
}
</script>我们想要模拟点击 class 为 button 的 div 标签。其实也就是让其触发 click 点击事件。可以使用 trigger 方法。
再前面加上 await,确保组件已经被更新。
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', async () => {
const wrapper = mount(testVueComp)
await wrapper.trigger('click')
expect(wrapper.text()).toBe('123')
// 也可以直接断言组件的 data 数据。相当于断言 this.msg。
expect(wrapper.vm.msg).toBe(123)
})断言组件向外触发了某个事件
总结
经过测试,发现:
如果在组件根标签上定义 click 点击事件,那么无论是否加修饰符 .stop,wrapper.emitted() 都会包含 click 事件。
如果不是在组件根标签上定义 click 点击事件,那么加了修饰符 .stop 会导致 wrapper.emitted() 不会包含 click 事件。
组件可以触发自定义事件,也可以触发原生事件(比如点击事件)。注意原生事件默认是会向上冒泡的。
比如我有下面这样一个组件。当我点击按钮时,会触发几个事件呢?正确答案是两个:一个原生冒泡事件 click,一个自定义事件 sayHello。
<template>
<div>
<button @click="add">按钮</button>
</div>
</template>
<script>
export default {
methods: {
add() {
this.$emit('sayHello', 123)
}
}
}
</script>我们使用 wrapper.emitted() 方法来断言:
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'
it('测试 vue 组件', async () => {
const wrapper = mount(testVueComp)
await wrapper.find('button').trigger('click')
console.log(wrapper.emitted())
})console.log(wrapper.emitted()) 打印如下。对象的 key 是对外触发的事件名,对象的 value 是一个数组,每触发一次该事件,都会往这个数组里推入触发该事件的入参(类型也为数组)。
{
sayHello: [ [ 123 ] ],
click: [ [ [MouseEvent] ] ]
}除此之外,还可以往 wrapper.emitted() 里传入参数,参数的含义与事件触发对象的 key 相同。
wrapper.find() 与 wrapper.get() 区别
find 如果找不到匹配选择器的元素,什么都不会做。
get 如果找不到匹配选择器的元素,会抛出异常。
断言默认插槽
const wrapper = mount(button, {
slots: {
// 默认插槽
default: 'Hello world'
}
})