Appearance
表单处理
¥Form Handling
Vue 中的表单可以像纯 HTML 表单一样简单,也可以像自定义 Vue 组件表单元素的复杂嵌套树一样简单。我们将逐步了解与表单元素交互、设置值和触发事件的方式。
¥Forms in Vue can be as simple as plain HTML forms to complicated nested trees of custom Vue component form elements. We will gradually go through the ways of interacting with form elements, setting values and triggering events.
我们最常用的方法是 setValue()
和 trigger()
。
¥The methods we will be using the most are setValue()
and trigger()
.
与表单元素交互
¥Interacting with form elements
让我们看一个非常基本的形式:
¥Let's take a look at a very basic form:
vue
<template>
<div>
<input type="email" v-model="email" />
<button @click="submit">Submit</button>
</div>
</template>
<script>
export default {
data() {
return {
email: ''
}
},
methods: {
submit() {
this.$emit('submit', this.email)
}
}
}
</script>
设置元素值
¥Setting element values
在 Vue 中将输入绑定到数据的最常见方法是使用 v-model
。你现在可能已经知道,它负责处理每个表单元素触发的事件以及它接受的属性,使我们可以轻松地使用表单元素。
¥The most common way to bind an input to data in Vue is by using v-model
. As you probably know by now, it takes care of what events each form element emits, and the props it accepts, making it easy for us to work with form elements.
要更改 VTU 中的输入值,可以使用 setValue()
方法。它接受一个参数,通常是 String
或 Boolean
,并返回 Promise
,该值在 Vue 更新 DOM 后解析。
¥To change the value of an input in VTU, you can use the setValue()
method. It accepts a parameter, most often a String
or a Boolean
, and returns a Promise
, which resolves after Vue has updated the DOM.
js
test('sets the value', async () => {
const wrapper = mount(Component)
const input = wrapper.find('input')
await input.setValue('my@mail.com')
expect(input.element.value).toBe('my@mail.com')
})
正如你所看到的,setValue
将输入元素上的 value
属性设置为我们传递给它的内容。
¥As you can see, setValue
sets the value
property on the input element to what we pass to it.
在我们做出任何断言之前,我们使用 await
来确保 Vue 已完成更新并且更改已反映在 DOM 中。
¥We are using await
to make sure that Vue has completed updating and the change has been reflected in the DOM, before we make any assertions.
触发事件
¥Triggering events
使用表单和操作元素时,触发事件是第二重要的操作。让我们看一下前面示例中的 button
。
¥Triggering events is the second most important action when working with forms and action elements. Let's take a look at our button
, from the previous example.
html
<button @click="submit">Submit</button>
要触发点击事件,我们可以使用 trigger
方法。
¥To trigger a click event, we can use the trigger
method.
js
test('trigger', async () => {
const wrapper = mount(Component)
// trigger the element
await wrapper.find('button').trigger('click')
// assert some action has been performed, like an emitted event.
expect(wrapper.emitted()).toHaveProperty('submit')
})
如果你以前没有看过
emitted()
,请不要担心。它用于断言组件触发的事件。你可以在 事件处理 中了解更多信息。¥If you haven't seen
emitted()
before, don't worry. It's used to assert the emitted events of a Component. You can learn more in Event Handling.
我们触发 click
事件监听器,以便组件执行 submit
方法。正如我们对 setValue
所做的那样,我们使用 await
来确保 Vue 反映该操作。
¥We trigger the click
event listener, so that the Component executes the submit
method. As we did with setValue
, we use await
to make sure the action is being reflected by Vue.
然后我们可以断言某些操作已经发生。在这种情况下,我们触发了正确的事件。
¥We can then assert that some action has happened. In this case, that we emitted the right event.
让我们将这两者结合起来来测试我们的简单表单是否正在触发用户输入。
¥Let's combine these two to test whether our simple form is emitting the user inputs.
js
test('emits the input to its parent', async () => {
const wrapper = mount(Component)
// set the value
await wrapper.find('input').setValue('my@mail.com')
// trigger the element
await wrapper.find('button').trigger('click')
// assert the `submit` event is emitted,
expect(wrapper.emitted('submit')[0][0]).toBe('my@mail.com')
})
高级工作流程
¥Advanced workflows
现在我们已经了解了基础知识,让我们深入研究更复杂的示例。
¥Now that we know the basics, let's dive into more complex examples.
使用各种表单元素
¥Working with various form elements
我们看到 setValue
适用于输入元素,但用途更广泛,因为它可以在各种类型的输入元素上设置值。
¥We saw setValue
works with input elements, but is much more versatile, as it can set the value on various types of input elements.
让我们看一下更复杂的表单,它有更多类型的输入。
¥Let's take a look at a more complicated form, which has more types of inputs.
vue
<template>
<form @submit.prevent="submit">
<input type="email" v-model="form.email" />
<textarea v-model="form.description" />
<select v-model="form.city">
<option value="new-york">New York</option>
<option value="moscow">Moscow</option>
</select>
<input type="checkbox" v-model="form.subscribe" />
<input type="radio" value="weekly" v-model="form.interval" />
<input type="radio" value="monthly" v-model="form.interval" />
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {
email: '',
description: '',
city: '',
subscribe: false,
interval: ''
}
}
},
methods: {
async submit() {
this.$emit('submit', this.form)
}
}
}
</script>
我们的扩展 Vue 组件有点长,有更多的输入类型,现在将 submit
处理程序移至 <form/>
元素。
¥Our extended Vue component is a bit longer, has a few more input types and now has the submit
handler moved to a <form/>
element.
与我们在 input
上设置值的方式相同,我们可以在表单中的所有其他输入上设置它。
¥The same way we set the value on the input
, we can set it on all the other inputs in the form.
js
import { mount } from '@vue/test-utils'
import FormComponent from './FormComponent.vue'
test('submits a form', async () => {
const wrapper = mount(FormComponent)
await wrapper.find('input[type=email]').setValue('name@mail.com')
await wrapper.find('textarea').setValue('Lorem ipsum dolor sit amet')
await wrapper.find('select').setValue('moscow')
await wrapper.find('input[type=checkbox]').setValue()
await wrapper.find('input[type=radio][value=monthly]').setValue()
})
正如你所看到的,setValue
是一种非常通用的方法。它可以与所有类型的表单元素一起使用。
¥As you can see, setValue
is a very versatile method. It can work with all types of form elements.
我们在任何地方都使用 await
,以确保在触发下一个更改之前已应用每个更改。建议这样做,以确保你在 DOM 更新时执行断言。
¥We are using await
everywhere, to make sure that each change has been applied before we trigger the next. This is recommended to make sure you do assertions when the DOM has updated.
提示
如果你没有将 OPTION
、CHECKBOX
或 RADIO
输入的参数传递给 setValue
,它们将设置为 checked
。
¥If you don't pass a parameter to setValue
for OPTION
, CHECKBOX
or RADIO
inputs, they will set as checked
.
我们已经在表单中设置了值,现在是时候提交表单并进行一些断言了。
¥We have set values in our form, now it's time to submit the form and do some assertions.
触发复杂的事件监听器
¥Triggering complex event listeners
事件监听器并不总是简单的 click
事件。Vue 允许你监听各种 DOM 事件,添加特殊修饰符,如 .prevent
等。让我们看看如何测试这些。
¥Event listeners are not always simple click
events. Vue allows you to listen to all kinds of DOM events, add special modifiers like .prevent
and more. Let's take a look how we can test those.
在上面的表单中,我们将事件从 button
移动到 form
元素。这是一个值得遵循的好习惯,因为这允许你通过点击 enter
键来提交表单,这是一种更原生的方法。
¥In our form above, we moved the event from the button
to the form
element. This is a good practice to follow, as this allows you to submit a form by hitting the enter
key, which is a more native approach.
为了触发 submit
处理程序,我们再次使用 trigger
方法。
¥To trigger the submit
handler, we use the trigger
method again.
js
test('submits the form', async () => {
const wrapper = mount(FormComponent)
const email = 'name@mail.com'
const description = 'Lorem ipsum dolor sit amet'
const city = 'moscow'
await wrapper.find('input[type=email]').setValue(email)
await wrapper.find('textarea').setValue(description)
await wrapper.find('select').setValue(city)
await wrapper.find('input[type=checkbox]').setValue()
await wrapper.find('input[type=radio][value=monthly]').setValue()
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.emitted('submit')[0][0]).toStrictEqual({
email,
description,
city,
subscribe: true,
interval: 'monthly'
})
})
为了测试事件修饰符,我们直接将事件字符串 submit.prevent
复制粘贴到 trigger
中。trigger
可以读取传递的事件及其所有修饰符,并有选择地应用必要的内容。
¥To test the event modifier, we directly copy-pasted our event string submit.prevent
into trigger
. trigger
can read the passed event and all its modifiers, and selectively apply what is necessary.
提示
原生事件修饰符(例如 .prevent
和 .stop
)是 Vue 特定的,因此我们不需要测试它们,Vue 内部已经这样做了。
¥Native event modifiers such as .prevent
and .stop
are Vue-specific and as such we don't need to test them, Vue internals do that already.
然后我们做出一个简单的断言,判断表单是否触发了正确的事件和有效负载。
¥We then make a simple assertion, whether the form emitted the correct event and payload.
原生表单提交
¥Native form submission
在 <form>
元素上触发 submit
事件会模仿表单提交期间的浏览器行为。如果我们想更自然地触发表单提交,我们可以在提交按钮上触发 click
事件。由于未连接到 document
的表单元素无法提交,根据 HTML 规范,我们需要使用 attachTo
来连接封装器的元素。
¥Triggering a submit
event on a <form>
element mimics browser behavior during form submission. If we wanted to trigger form submission more naturally, we could trigger a click
event on the submit button instead. Since form elements not connected to the document
cannot be submitted, as per the HTML specification, we need to use attachTo
to connect the wrapper's element.
同一事件的多个修饰符
¥Multiple modifiers on the same event
假设你有一个非常详细且复杂的表单,具有特殊的交互处理。我们该如何测试呢?
¥Let's assume you have a very detailed and complex form, with special interaction handling. How can we go about testing that?
html
<input @keydown.meta.c.exact.prevent="captureCopy" v-model="input" />
假设我们有一个处理用户点击 cmd
+ c
的输入,并且我们想要拦截并阻止他进行复制。测试这一点就像将事件从组件复制并粘贴到 trigger()
方法一样简单。
¥Assume we have an input that handles when the user clicks cmd
+ c
, and we want to intercept and stop him from copying. Testing this is as easy as copy & pasting the event from the Component to the trigger()
method.
js
test('handles complex events', async () => {
const wrapper = mount(Component)
await wrapper.find(input).trigger('keydown.meta.c.exact.prevent')
// run your assertions
})
Vue Test Utils 读取事件并将适当的属性应用于事件对象。在这种情况下,它将匹配如下内容:
¥Vue Test Utils reads the event and applies the appropriate properties to the event object. In this case it will match something like this:
js
{
// ... other properties
"key": "c",
"metaKey": true
}
向事件添加额外数据
¥Adding extra data to an event
假设你的代码需要 event
对象内部的某些内容。你可以通过传递额外的数据作为第二个参数来测试此类场景。
¥Let's say your code needs something from inside the event
object. You can test such scenarios by passing extra data as a second parameter.
vue
<template>
<form>
<input type="text" v-model="value" @blur="handleBlur" />
<button>Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
value: ''
}
},
methods: {
handleBlur(event) {
if (event.relatedTarget.tagName === 'BUTTON') {
this.$emit('focus-lost')
}
}
}
}
</script>
js
import Form from './Form.vue'
test('emits an event only if you lose focus to a button', () => {
const wrapper = mount(Form)
const componentToGetFocus = wrapper.find('button')
wrapper.find('input').trigger('blur', {
relatedTarget: componentToGetFocus.element
})
expect(wrapper.emitted('focus-lost')).toBeTruthy()
})
这里我们假设我们的代码检查 event
对象内部,relatedTarget
是否是按钮。我们可以简单地传递对此类元素的引用,模拟用户在 input
中输入内容后单击 button
会发生的情况。
¥Here we assume our code checks inside the event
object, whether the relatedTarget
is a button or not. We can simply pass a reference to such an element, mimicking what would happen if the user clicks on a button
after typing something in the input
.
与 Vue 组件输入交互
¥Interacting with Vue Component inputs
输入不仅仅是简单的元素。我们经常使用行为类似于输入的 Vue 组件。他们可以以易于使用的格式添加标记、样式和许多功能。
¥Inputs are not only plain elements. We often use Vue components that behave like inputs. They can add markup, styling and lots of functionalities in an easy to use format.
测试使用此类输入的表单一开始可能会令人畏惧,但只要遵循一些简单的规则,它很快就会变得轻而易举。
¥Testing forms that use such inputs can be daunting at first, but with a few simple rules, it quickly becomes a walk in the park.
以下是封装 label
和 input
元素的组件:
¥Following is a Component that wraps a label
and an input
element:
vue
<template>
<label>
{{ label }}
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</label>
</template>
<script>
export default {
name: 'CustomInput',
props: ['modelValue', 'label']
}
</script>
这个 Vue 组件还会返回你输入的任何内容。要使用它,你需要执行以下操作:
¥This Vue component also emits back whatever you type. To use it you do:
html
<custom-input v-model="input" label="Text Input" class="text-input" />
如上所述,大多数由 Vue 驱动的输入都有真正的 button
或 input
。你可以轻松找到该元素并对其进行操作:
¥As above, most of these Vue-powered inputs have a real button
or input
in them. You can just as easily find that element and act on it:
js
test('fills in the form', async () => {
const wrapper = mount(CustomInput)
await wrapper.find('.text-input input').setValue('text')
// continue with assertions or actions like submit the form, assert the DOM…
})
测试复杂的输入组件
¥Testing complex Input components
如果你的输入组件不那么简单会发生什么?你可能正在使用 UI 库,例如 Vuetify。如果你依赖挖掘标记内部来查找正确的元素,那么如果外部库决定更改其内部结构,你的测试可能会中断。
¥What happens if your Input component is not that simple? You might be using a UI library, like Vuetify. If you rely on digging inside the markup to find the right element, your tests may break if the external library decides to change their internals.
在这种情况下,你可以使用组件实例和 setValue
直接设置该值。
¥In such cases you can set the value directly, using the component instance and setValue
.
假设我们有一个使用 Vuetify 文本区域的表单:
¥Assume we have a form that uses the Vuetify textarea:
vue
<template>
<form @submit.prevent="handleSubmit">
<v-textarea v-model="description" ref="description" />
<button type="submit">Send</button>
</form>
</template>
<script>
export default {
name: 'CustomTextarea',
data() {
return {
description: ''
}
},
methods: {
handleSubmit() {
this.$emit('submitted', this.description)
}
}
}
</script>
我们可以使用 findComponent
来找到组件实例,然后设置它的值。
¥We can use findComponent
to find the component instance, and then set its value.
js
test('emits textarea value on submit', async () => {
const wrapper = mount(CustomTextarea)
const description = 'Some very long text...'
await wrapper.findComponent({ ref: 'description' }).setValue(description)
wrapper.find('form').trigger('submit')
expect(wrapper.emitted('submitted')[0][0]).toEqual(description)
})
结论
¥Conclusion
使用
setValue
设置 DOM 输入和 Vue 组件上的值。¥Use
setValue
to set the value on both DOM inputs and Vue components.使用
trigger
触发 DOM 事件,无论带修饰符还是不带修饰符。¥Use
trigger
to trigger DOM events, both with and without modifiers.使用第二个参数将额外的事件数据添加到
trigger
。¥Add extra event data to
trigger
using the second parameter.断言 DOM 发生了变化并且触发了正确的事件。尽量不要在组件实例上断言数据。
¥Assert that the DOM changed and the right events got emitted. Try not to assert data on the Component instance.