Test Vue.js Slots in Jest
测试Vue.js中的Slots插槽
Learn how to test content distributed using slots and named slots.
了解如何测试使用插槽和具名插槽分发的内容。
Slots are the way to make content distribution happen in the web components world. Vue.js slots are made following the Web Component specs, meaning that if you learn how to use them in Vue.js, that will be useful in the future.
插槽是Web组件化领域内分发内容的经典方式。Vue.js的slots顺应Web组件化的步伐,只要你学会了如何运用,那么未来必有大用。
They make components structure to be much more flexible, moving the responsibility of managing the state to the parent component. For example, we can have a List component, and different kind of item components, such ListItem and ListItemImage. They’ll be used like:
Slots使得组件的结构更加灵活,将管理数据状态的责任交给了父组件。举例来说,我们可以创建一个具有不同类型子组件的列表组件,就像下面代码中的ListItem 和 ListItemImage一样。使用方法如下:
<template>
<List>
<ListItem :someProp="someValue" />
<ListItem :someProp="someValue" />
<ListItemImage :image="imageUrl" :someProp="someValue" />
</List>
</template>
The inner content of List is the slot itself, and its accessible via <slot>
tag. So the List implementation looks like:
List组件内部的内容就是它自己的插槽,并且可以通过<slot>
标签更简单地实现预期效果:
<template>
<ul>
<!-- slot here will equal to what\'s inside <List> -->
<slot></slot>
</ul>
</template>
And, say that the ListItem component looks like:
ListItem组件如下:
<template>
<li> {{ someProp }} </li>
</template>
Then, the final result rendered by Vue.js would be:
所以,最终Vue会渲染出如下结果:
<ul>
<li> someValue </li>
<li> someValue </li>
<li> someValue </li>
<!-- assume the same implementation 'for' ListItemImage -->
</ul>
Make MessageList slot based
基于MessageList使用插槽
Let’s take a look at the MessageList.vue component:
我们先看一下MessageList组件结构:
<template>
<ul>
<Message @message-clicked="handleMessageClick"
:message="message"
v-for="message in messages"
:key="message"/>
</ul>
</template>
MessageList has “hardcoded” the Message component inside. In a way that’s more automated, but in the other is not flexible at all. What if you wanna have different types of Message components? What about changing its structure or styling? That’s where slots come in handy.
MessageList组件内是对Message组件的硬编码。虽然如此一来,自动化程度更高,但是反过来说,灵活度就有些不足。如果你想有不同种类的子组件呢?或者想改变结构或样式呢?这就需要slots出场了:
Let’s change Message.vue to use slots. First, move that <Message... part to the App.vue component, as well as the handleMessageClick method, so it’s used externally:
我们用slots改写一下Message组件,首先,把<Message>
标签内的内容直接挪到根组件中,别忘了把绑定的点击函数handleMessageClick也挪过来:
<template>
<div id="app">
<MessageList>
<Message
@message-clicked="handleMessageClick"
:message="message"
v-for="message in messages"
:key="message"/>
</MessageList>
</div>
</template>
<script>
import MessageList from './components/MessageList'
import Message from './components/Message'
export default {
name: 'app',
data: () => ({ messages: ['Hey John', 'Howdy Paco'] }),
methods: {
handleMessageClick(message) {
console.log(message)
}
},
components: {
MessageList,
Message
}
}
</script>
Don’t forget to import the Message component and add it to the components option in App.vue.
同时也需要在App组件中引入并注册Message组件。
Then, in MessageList.vue, we can remove the references to Message, looking like:
然后,在MessageList组件中我们就可以删掉Message组件的引用了,用<slot>
代替:
<template>
<ul class="list-messages">
<slot></slot>
</ul>
</template>
<script>
export default {
name: 'MessageList'
}
</script>
children andslots
¥children 和 $slots对象
Vue components have two instance variables useful for accessing slots:
Vue组件给我们暴露了两个索引slots的途径:
- $children: an array of Vue component instances of the default slot.
- $children:是由当前Vue实例的直接子组件组成的数组
- $slots: an object of VNodes mapping all the slots defined in the component instance.
- $slots:是包括所有组件实例中定义的插槽的映射组成的对象
The slots object has more data available. In fact,children is just a portion of the slots variable, that could be accessed the same way by mapping over theslots.default array, filtered by Vue component instances:
slots对象还有更多的可访问数据。实际上,children只是slots对象的组成部分,可以通过slots.default数组映射来访问经过Vue组件实例筛选后的$children数组。
const children = this.$slots.default
.map(vnode => vnode.componentInstance)
.filter(cmp => !!cmp)
Testing Slots
测试Slots插槽
Probably what we want to test the most out of slots is where they end up in the component, and for that we can reuse the skills got in the article Test Styles and Structure of Vue.js Components in Jest.
也许我们对slots最想测的是插槽内容是否成功渲染在父组件中,我们可以用之前的测试技巧,来实现对插槽的测试。
Right now, most of the tests in MessageList.test.js will fail, so let’s remove them all (or comment them out), and focus on slot testing.
现在,大部分MessageList.test.js文件中的测试都会失败报错,所以我们先将之前写的用例删掉,只关注对slots的测试。
One thing we can test, is to make sure that the Message components end up within a ul element with class list-messages. In order to pass slots to the MessageList component, we can use the slots property of the options object of mount or shallow methods. So let’s create a beforeEach method with the following code:
有一件事要注意,那就是要确保Message组件是以类名为list-messages
的ul标签作为结尾。为了将slots的内容传到MessageList组件中,我们需要将slots封装为对象作为参数传入mount 或 shallow方法中。现在我们在代码中修改beforeEach方法:
beforeEach(() => {
cmp = mount(MessageList, {
slots: {
default: '<div class="fake-msg"></div>'
}
})
})
Since we just want to test if the messages are rendered, we can search for <div class="fake-msg"></div>
as follows:
因为我们只是想看messages是否被渲染成功,所以我们只需要断言<div class="fake-msg"></div>
节点是否符合预期。
it('Messages are inserted in a ul.list-messages element', () => {
const list = cmp.find('ul.list-messages')
expect(list.findAll('.fake-msg').length).toBe(1)
})
And that should be ok to go. The slots option also accepts a component declaration, and even an array, so we could write:
这样以来测试就能顺利执行了。slots配置也能接收一个组件声明表达式,甚至是一个数组:
import AnyComponent from 'anycomponent'
...
mount(MessageList, {
slots: {
default: AnyComponent // or [AnyComponent, AnyComponent]
}
})
The problem with that is that is very limited, you cannot override props for example, and we need that for the Message component since it has a required property. This should affect the cases that you really need to test slots with the expected components. For example, if you wanna make sure that MessageList expects only Message components as slots. That’s on track and at some point it will land in vue-test-utils.
这样做有一个问题,就是限制性太强了。比如你不能重写props,Message组件中props是一个必备属性。这就使得你不能在你想测的组件中测试slots属性,就像你想核实一下MessageList组件仅仅将Message组件作为插槽的时候。想解决这个问题,还是要求助于vue-test-utils
。
As a workaround, we can accomplish that by using a render function. So we can rewrite the test to be more specific:
解决方法就是用渲染函数,代码做如下改写:
beforeEach(() => {
const messageWrapper = {
render(h) {
return h(Message, { props: { message: 'hey' } })
}
}
cmp = mount(MessageList, {
slots: {
default: messageWrapper
}
})
})
it('Messages are inserted in a ul.list-messages element', () => {
const list = cmp.find('ul.list-messages')
expect(list.find(Message).isVueInstance()).toBe(true)
})
Testing Named Slots
对具名Slots测试
The unnamed slot we used above is called the default slot, but we can have multiple slots by using named exports.
上文中我们使用的是默认slots,不是具名插槽,但是我们可以通过插槽的name属性,在父组件使用具名插槽。
Let’s add a header to the MessageList.vue component:
我们现在在MessageList组件中添加一个header的name属性:
<template>
<div>
<header class="list-header">
<slot name="header">
This is a default header
</slot>
</header>
<ul class="list-messages">
<slot></slot>
</ul>
</div>
</template>
By using <slot name="header">
we’re defining another slot for the header. You can see a This is a default header text inside the slot, that’s displayed as the default content when a slot is not passed to the component, and that’s applicable to the default slot.
通过 <slot name="header">
我们可以为header定义另一个插槽。你可以看到插槽内的内容是"This is a default header",当slot没有被传入组件内,这段文字就会作为默认文本被渲染,这个规则适用于默认slots。
Then, from App.vue we can use add a header to the MessageList component by using the slot="header" attribute:
然后,在App组件中,我们可以在MessageList组件内部添加一个header的标签,slot属性设置为header:
<template>
<div id="app">
<MessageList>
<header slot="header">
Awesome header
</header>
<Message
@message-clicked="handleMessageClick"
:message="message"
v-for="message in messages"
:key="message"/>
</MessageList>
</div>
</template>
It’s time to write a unit test for it. Testing named slots is just as testing a default slot, the same dynamics apply. So, we can start by testing that the header slot is rendered within the <header class="list-header">
element, and it renders a default text when no header slot is passed by. In MessageList.test.js:
现在可以为以上代码写单元测试了。测试具名slots与测试默认slots原理类似。所以,我们可以检测header插槽有没有在<header class="list-header">
节点内被渲染,并且如果在没有header插槽被传入子组件时,默认内容会被渲染。在MessageList.test.js中可以写如下代码:
it('Header slot renders a default header text', () => {
const header = cmp.find('.list-header')
expect(header.text().trim()).toBe('This is a default header')
})
Then, the same but checking the default content gets replaced when we mock the header slot:
然后,同样我们检测默认内容是否被替换为模拟的header插槽内容:
it('Header slot is rendered withing .list-header', () => {
const component = mount(MessageList, {
slots: {
header: '<div>What an awesome header</div>'
}
})
const header = component.find('.list-header')
expect(header.text().trim()).toBe('What an awesome header')
})
See that the header slot used in this last test is wrapped in a <div>. It’s important the slots are wrapped in an html tag, otherwise vue-test-utils will complain.
可以看到header插槽在最后边这个测试中包裹在一个div标签中。对于插槽来说,被一个html标签包裹是很重要的事,否则,vue-test-utils就会报错了。
Testing Contextual Slot Specs
组件上下文中插槽的测试规范
We’ve test how and where the slots render, and probably that’s what we mostly need. However, it doesn’t end there. If you pass component instances as slots, just as we’re doing in the default slot with Message, you can test functionality related to it.
我们已经测了插槽如何与在何处渲染,这也许已经覆盖了我们的需求。然而,事情还没结束。如果你将一个组件实例作为slot传给一个组件,就像我们Message组件做的那样,你可以测试与之相关的功能。
Be careful on what you test here, this is probably something you don’t need to do in most cases, since the functional tests of a component should belong to that component test.
此处要留意,这个用例也许在大多数情况下都不必做,因为组件的功能性测试应该包含在相关组件的测试用例中。
When talking about testing slots functionality, we test how a slot must behave in the context of the component where that slot is used, and that’s something is not very common.
当我们谈及slots的功能性测试,我们只是对slots在其所在的组件上下文中如何表现,这种测试还真不太常见。
Normally we just pass the slot and forget about it. So don’t get too stick to the following example, It’s only purpose is to demonstrate how the tool works.
正常情况下,我们只是将slot传过去,然后就丢到了一边。所以不要拘泥于下面这个用例设计,这只是为了向大家展示相关工具怎么使用的。
Let’s say that, for whatever reason, in the context of the MessageList component, all the Message components must have a length higher than 5. We can test that like:
不得不说,不管什么缘由,在MessageList组件的上下文中,所有Message组件的字符长度都要大于5个。我可以像下面这样测试:
it('Message length is higher than 5', () => {
const messages = cmp.findAll(Message)
messages.wrappers.forEach(c => {
expect(c.vm.message.length).toBeGreaterThan(5)
})
})
findAll returns an object containing an array of wrappers where we can access its vm component instance property. This test will fail because the message has a length of 3, so go to the beforeEach function and make it longer:
findAll方法返回了一个对象,这个对象包含了一个数组,数组中包含了所有Message组件的容器,如此一来我们就可以访问组件实例中的属性了。下方的测试会失败,原因是message项长度只有三个,所以在beforeEach方法中调整一下初始化长度:
beforeEach(() => {
const messageWrapper = {
render(h) {
return h(Message, { props: { message: 'hey yo' } })
}
}
})
...
Then it should pass.
现在就可以通过测试了。
Conclusion
总结
Testing slots is very simple, normally we’d like to test that they’re placed and rendered as we want, so is just like testing style and structure knowing how slots behave or can be mocked.
测试插槽很简单,正常情况下我们更偏向于测试插槽内容是否如期被替换或被渲染,所以就像测试样式和结构一样,我们只需要知道slots的反应如何和可以被模拟就可以了。
You won’t need to test slot functionality very ofter probably. Keep in mind to test things only related to slots when you want to test slots, and think twice if what you’re testing belongs to the slot test or the component test itself.
而且一般您也不再需要对slot进行功能性测试了。要注意的是当您要测试插槽时,只需要测试与插槽相关的事物,并且如果您正在测试的内容既属于插槽测试,有属于组件测试的层面,请考虑设计两个测试用例。