跳至内容

表单处理

Vue 中的表单可以像简单的 HTML 表单一样简单,也可以像复杂的嵌套自定义 Vue 组件表单元素树一样复杂。我们将逐步介绍与表单元素交互、设置值和触发事件的方法。

我们将使用最多的方法是 setValue()trigger()

与表单元素交互

让我们看一个非常基本的表单

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>

设置元素值

在 Vue 中将输入绑定到数据的最常见方法是使用 v-model。正如您可能已经知道的,它负责每个表单元素发出的事件以及它接受的道具,使我们能够轻松地使用表单元素。

要更改 VTU 中输入的值,您可以使用 setValue() 方法。它接受一个参数,通常是 StringBoolean,并返回一个 Promise,该 Promise 在 Vue 更新 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 属性设置为我们传递给它的内容。

我们使用 await 来确保 Vue 已完成更新并且更改已反映在 DOM 中,然后我们进行任何断言。

触发事件

触发事件是在处理表单和操作元素时第二重要的操作。让我们看看我们之前的示例中的 button

html
<button @click="submit">Submit</button>

要触发点击事件,我们可以使用 trigger 方法。

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(),不用担心。它用于断言组件发出的事件。您可以在 事件处理 中了解更多信息。

我们触发 click 事件监听器,以便组件执行 submit 方法。与 setValue 一样,我们使用 await 来确保操作反映在 Vue 中。

然后我们可以断言某些操作已经发生。在这种情况下,我们发出了正确的事件。

让我们将这两者结合起来,测试我们简单的表单是否正在发射用户输入。

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')
})

高级工作流程

现在我们知道了基础知识,让我们深入研究更复杂的示例。

使用各种表单元素

我们看到 setValue 可以与输入元素一起使用,但它更加通用,因为它可以设置各种类型输入元素的值。

让我们看一个更复杂的表单,它包含更多类型的输入。

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/> 元素中。

与我们在 input 上设置值的方式相同,我们可以在表单中的所有其他输入上设置值。

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 是一种非常通用的方法。它可以与所有类型的表单元素一起使用。

我们在所有地方都使用 await 来确保在触发下一个更改之前应用每个更改。建议这样做,以确保您在 DOM 更新后进行断言。

提示

如果您没有为 OPTIONCHECKBOXRADIO 输入传递参数到 setValue,它们将被设置为 checked

我们已在表单中设置了值,现在是时候提交表单并进行一些断言了。

触发复杂的事件监听器

事件监听器并不总是简单的 click 事件。Vue 允许您监听各种 DOM 事件,添加特殊的修饰符,如 .prevent 等等。让我们看看如何测试它们。

在我们上面的表单中,我们将事件从 button 元素移到了 form 元素。这是一个很好的做法,因为它允许您通过按 enter 键提交表单,这是一种更原生的方法。

要触发 submit 处理程序,我们再次使用 trigger 方法。

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 可以读取传递的事件及其所有修饰符,并有选择地应用必要的内容。

提示

.prevent.stop 这样的原生事件修饰符是 Vue 特定的,因此我们不需要测试它们,Vue 内部已经完成了这些工作。

然后我们进行一个简单的断言,即表单是否发出了正确的事件和有效负载。

原生表单提交

<form> 元素上触发 submit 事件模拟了表单提交期间的浏览器行为。如果我们想更自然地触发表单提交,我们可以改为在提交按钮上触发 click 事件。由于根据 HTML 规范,未连接到 document 的表单元素无法提交,因此我们需要使用 attachTo 来连接包装器的元素。

同一事件上的多个修饰符

假设您有一个非常详细和复杂的表单,具有特殊的交互处理。我们如何测试它呢?

html
<input @keydown.meta.c.exact.prevent="captureCopy" v-model="input" />

假设我们有一个输入,它处理用户点击 cmd + c 的情况,我们想要拦截并阻止他复制。测试它就像将事件从组件复制粘贴到 trigger() 方法一样简单。

js
test('handles complex events', async () => {
  const wrapper = mount(Component)

  await wrapper.find(input).trigger('keydown.meta.c.exact.prevent')

  // run your assertions
})

Vue 测试工具读取事件并将适当的属性应用于事件对象。在这种情况下,它将匹配类似以下内容

js
{
  // ... other properties
  "key": "c",
  "metaKey": true
}

向事件添加额外数据

假设您的代码需要事件对象中的某些内容。您可以通过将额外数据作为第二个参数传递来测试此类场景。

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 是否是按钮。我们可以简单地传递对该元素的引用,模拟用户在输入框中输入内容后点击 button 时会发生的情况。

与 Vue 组件输入交互

输入不仅仅是普通的元素。我们经常使用像输入一样的 Vue 组件。它们可以以易于使用的方式添加标记、样式和许多功能。

测试使用此类输入的表单乍一看可能令人生畏,但只需遵循几个简单的规则,它就会变得轻而易举。

下面是一个包装 labelinput 元素的组件

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 组件还会发射您键入的任何内容。要使用它,您需要执行以下操作

html
<custom-input v-model="input" label="Text Input" class="text-input" />

如上所述,大多数这些 Vue 驱动的输入都包含一个真正的 buttoninput。您可以轻松地找到该元素并对其进行操作

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…
})

测试复杂的输入组件

如果您的输入组件没有那么简单怎么办?您可能正在使用 UI 库,例如 Vuetify。如果您依赖于深入标记以找到正确的元素,那么如果外部库决定更改其内部结构,您的测试可能会失败。

在这种情况下,您可以使用组件实例和 setValue 直接设置值。

假设我们有一个使用 Vuetify 文本区域的表单

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 找到组件实例,然后设置其值。

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)
})

结论

  • 使用 setValue 为 DOM 输入和 Vue 组件设置值。
  • 使用 trigger 触发 DOM 事件,无论是否使用修饰符。
  • 使用第二个参数将额外事件数据添加到 trigger 中。
  • 断言 DOM 已更改并且发出了正确的事件。尽量不要在组件实例上断言数据。