表单处理
Vue 中的表单可以像简单的 HTML 表单一样简单,也可以像复杂的嵌套自定义 Vue 组件表单元素树一样复杂。我们将逐步介绍与表单元素交互、设置值和触发事件的方法。
我们将使用最多的方法是 setValue()
和 trigger()
。
与表单元素交互
让我们看一个非常基本的表单
<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>
设置元素值
在 Vue 中将输入绑定到数据的最常见方法是使用 v-model
。正如您可能已经知道的,它负责每个表单元素发出的事件以及它接受的道具,使我们能够轻松地使用表单元素。
要更改 VTU 中输入的值,您可以使用 setValue()
方法。它接受一个参数,通常是 String
或 Boolean
,并返回一个 Promise
,该 Promise
在 Vue 更新 DOM 后解析。
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
属性设置为我们传递给它的内容。
我们使用 await
来确保 Vue 已完成更新并且更改已反映在 DOM 中,然后我们进行任何断言。
触发事件
触发事件是在处理表单和操作元素时第二重要的操作。让我们看看我们之前的示例中的 button
。
<button @click="submit">Submit</button>
要触发点击事件,我们可以使用 trigger
方法。
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()
,不用担心。它用于断言组件发出的事件。您可以在 事件处理 中了解更多信息。
我们触发 click
事件监听器,以便组件执行 submit
方法。与 setValue
一样,我们使用 await
来确保操作反映在 Vue 中。
然后我们可以断言某些操作已经发生。在这种情况下,我们发出了正确的事件。
让我们将这两者结合起来,测试我们简单的表单是否正在发射用户输入。
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')
})
高级工作流程
现在我们知道了基础知识,让我们深入研究更复杂的示例。
使用各种表单元素
我们看到 setValue
可以与输入元素一起使用,但它更加通用,因为它可以设置各种类型输入元素的值。
让我们看一个更复杂的表单,它包含更多类型的输入。
<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/>
元素中。
与我们在 input
上设置值的方式相同,我们可以在表单中的所有其他输入上设置值。
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
是一种非常通用的方法。它可以与所有类型的表单元素一起使用。
我们在所有地方都使用 await
来确保在触发下一个更改之前应用每个更改。建议这样做,以确保您在 DOM 更新后进行断言。
提示
如果您没有为 OPTION
、CHECKBOX
或 RADIO
输入传递参数到 setValue
,它们将被设置为 checked
。
我们已在表单中设置了值,现在是时候提交表单并进行一些断言了。
触发复杂的事件监听器
事件监听器并不总是简单的 click
事件。Vue 允许您监听各种 DOM 事件,添加特殊的修饰符,如 .prevent
等等。让我们看看如何测试它们。
在我们上面的表单中,我们将事件从 button
元素移到了 form
元素。这是一个很好的做法,因为它允许您通过按 enter
键提交表单,这是一种更原生的方法。
要触发 submit
处理程序,我们再次使用 trigger
方法。
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
可以读取传递的事件及其所有修饰符,并有选择地应用必要的内容。
提示
像 .prevent
和 .stop
这样的原生事件修饰符是 Vue 特定的,因此我们不需要测试它们,Vue 内部已经完成了这些工作。
然后我们进行一个简单的断言,即表单是否发出了正确的事件和有效负载。
原生表单提交
在 <form>
元素上触发 submit
事件模拟了表单提交期间的浏览器行为。如果我们想更自然地触发表单提交,我们可以改为在提交按钮上触发 click
事件。由于根据 HTML 规范,未连接到 document
的表单元素无法提交,因此我们需要使用 attachTo
来连接包装器的元素。
同一事件上的多个修饰符
假设您有一个非常详细和复杂的表单,具有特殊的交互处理。我们如何测试它呢?
<input @keydown.meta.c.exact.prevent="captureCopy" v-model="input" />
假设我们有一个输入,它处理用户点击 cmd
+ c
的情况,我们想要拦截并阻止他复制。测试它就像将事件从组件复制粘贴到 trigger()
方法一样简单。
test('handles complex events', async () => {
const wrapper = mount(Component)
await wrapper.find(input).trigger('keydown.meta.c.exact.prevent')
// run your assertions
})
Vue 测试工具读取事件并将适当的属性应用于事件对象。在这种情况下,它将匹配类似以下内容
{
// ... other properties
"key": "c",
"metaKey": true
}
向事件添加额外数据
假设您的代码需要事件对象中的某些内容。您可以通过将额外数据作为第二个参数传递来测试此类场景。
<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>
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
是否是按钮。我们可以简单地传递对该元素的引用,模拟用户在输入框中输入内容后点击 button
时会发生的情况。
与 Vue 组件输入交互
输入不仅仅是普通的元素。我们经常使用像输入一样的 Vue 组件。它们可以以易于使用的方式添加标记、样式和许多功能。
测试使用此类输入的表单乍一看可能令人生畏,但只需遵循几个简单的规则,它就会变得轻而易举。
下面是一个包装 label
和 input
元素的组件
<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 组件还会发射您键入的任何内容。要使用它,您需要执行以下操作
<custom-input v-model="input" label="Text Input" class="text-input" />
如上所述,大多数这些 Vue 驱动的输入都包含一个真正的 button
或 input
。您可以轻松地找到该元素并对其进行操作
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…
})
测试复杂的输入组件
如果您的输入组件没有那么简单怎么办?您可能正在使用 UI 库,例如 Vuetify。如果您依赖于深入标记以找到正确的元素,那么如果外部库决定更改其内部结构,您的测试可能会失败。
在这种情况下,您可以使用组件实例和 setValue
直接设置值。
假设我们有一个使用 Vuetify 文本区域的表单
<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
找到组件实例,然后设置其值。
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)
})
结论
- 使用
setValue
为 DOM 输入和 Vue 组件设置值。 - 使用
trigger
触发 DOM 事件,无论是否使用修饰符。 - 使用第二个参数将额外事件数据添加到
trigger
中。 - 断言 DOM 已更改并且发出了正确的事件。尽量不要在组件实例上断言数据。