Skip to main content
Cypress应用

Vue 示例

info
你将学习到
  • 如何在 Cypress 中挂载 Vue 组件
  • 如何向组件传递 props 和事件
  • 如何在组件中使用插槽
  • 如何将 Vue Test Utils 与 Cypress 结合使用
  • 如何为 Vue 自定义 cy.mount()

挂载组件

使用 cy.mount()

要使用 cy.mount() 挂载组件,需导入组件并将其传递给该方法:

import { Stepper } from './Stepper.vue'

it('mounts', () => {
cy.mount(Stepper)
})

向组件传递数据

可以通过在选项中设置 props 来向组件传递 props 和事件:

cy.mount(Stepper, {
props: {
initial: 100,
},
})

测试事件处理器

将 Cypress 的 spy 传递给事件 prop 并验证其是否被调用:

it('clicking + fires a change event with the incremented value', () => {
const onChangeSpy = cy.spy().as('onChangeSpy')
cy.mount(Stepper, { props: { onChange: onChangeSpy } })
cy.get('[data-cy=increment]').click()
cy.get('@onChangeSpy').should('have.been.calledWith', 1)
})

使用 JSX

mount 命令也支持 JSX 语法(前提是你已配置打包工具支持转译 JSX 或 TSX 文件)。有些人可能会发现使用 JSX 语法在编写测试时更有优势。

JSX 示例:

it('clicking + fires a change event with the incremented value', () => {
const onChangeSpy = cy.spy().as('onChangeSpy')
cy.mount(<Stepper initial={100} onChange={onChangeSpy} />)
cy.get('[data-cy=increment]').click()
cy.get('@onChangeSpy').should('have.been.calledWith', 101)
})

使用插槽

默认插槽

import DefaultSlot from './DefaultSlot.vue'

describe('<DefaultSlot />', () => {
it('renders', () => {
cy.mount(DefaultSlot, {
slots: {
default: 'Hello there!',
},
})
cy.get('div.content').should('have.text', 'Hello there!')
})
})

具名插槽

import NamedSlot from './NamedSlot.vue'

describe('<NamedSlot />', () => {
it('renders', () => {
const slots = {
header: 'my header',
footer: 'my footer',
}
cy.mount(NamedSlot, {
slots,
})
cy.get('header').should('have.text', 'my header')
cy.get('footer').should('have.text', 'my footer')
})
})

有关使用插槽测试 Vue 组件的更多信息,请参考 Vue Test Utils 插槽指南

使用 Vue Test Utils

为了鼓励现有组件测试与 Cypress 之间的互操作性,我们支持使用 Vue Test Utils 的 API。

cy.mount(Stepper).then(({ wrapper, component }) => {
// `wrapper` 是 Vue Test Utils 的包装器
// `component` 是组件实例本身
})

如果你打算频繁使用 wrapper 并利用 Vue Test Utils 的 API,我们建议你编写一个 自定义挂载命令 并为 wrapper 创建一个 Cypress 别名。

import { mount } from 'cypress/vue'

Cypress.Commands.add('mount', (...args) => {
return mount(...args).then(({ wrapper }) => {
return cy.wrap(wrapper).as('vue')
})
})

// "@vue" 别名现在可以在任何地方使用
// 在你挂载组件之后
cy.mount(Stepper).doStuff().get('@vue') // 现在主题是 Vue 包装器

这意味着你可以访问 mount 命令返回的 wrapper,并使用 wrapper.emitted() 来获取组件测试中触发的原生 DOM 事件以及自定义事件。

因为 wrapper.emitted() 只是数据,而不是基于 spy 的,所以你需要解包其结果来编写断言。

你的测试失败信息不会那么有帮助,因为你无法使用 Cypress 内置的 Sinon-Chai 库,该库提供了诸如 to.have.been.calledto.have.been.calledWith 等方法。

使用 cy.get('@vue') 别名的代码可能如下所示。

注意我们使用 'should' 函数签名来利用 Cypress 的 重试机制。如果我们使用 cy.then 而不是 cy.should 进行链式调用,可能会遇到 Vue Test Utils 测试中的那种问题,即你需要频繁使用 await 来确保 DOM 已更新或任何响应式事件已触发。

cy.mount(Stepper, { props: { initial: 100 } })
cy.get(incrementSelector).click()
cy.get('@vue').should(({ wrapper }) => {
expect(wrapper.emitted('change')).to.have.length
expect(wrapper.emitted('change')[0][0]).to.equal('101')
})

尽管我们推荐使用 spies 而不是 Vue Test Utils 的内部 API,但你可能决定继续使用 emitted,因为它会自动记录组件发出的每一个事件,因此你无需为每个发出的事件创建 spy。

这种自动 spy 行为对于发出许多自定义事件的组件可能很有用。

自定义挂载命令

自定义 cy.mount()

虽然你可以在测试中使用 mount() 函数,但我们推荐使用 cy.mount(),这是一个定义在 cypress/support/component.js 文件中的 自定义命令

import { mount } from 'cypress/vue'

Cypress.Commands.add('mount', mount)

这允许你在任何测试中使用 cy.mount(),而无需在每个规范文件中导入 mount() 函数。

默认情况下,cy.mount()mount() 的简单传递,但是你可以自定义 cy.mount() 以满足你的需求。例如,如果你在 Vue 应用中使用插件或其他全局应用级设置,可以在这里配置它们。

以下是几个演示使用自定义挂载命令的示例。这些示例可以调整为支持大多数其他提供者。

复制插件

大多数应用程序会有状态管理或路由。这两者都是 Vue 插件。

import { createPinia } from 'pinia' // 或 Vuex
import { createI18n } from 'vue-i18n'
import { mount } from 'cypress/vue'
import { h } from 'vue'

// 我们建议你将这个提取出来
// 到一个与你的 main.js 文件共享的常量文件中。
const i18nOptions = {
locale: 'en',
messages: {
en: {
hello: 'hello!',
},
ja: {
hello: 'こんにちは!',
},
},
}

Cypress.Commands.add('mount', (component, ...args) => {
args.global = args.global || {}
args.global.plugins = args.global.plugins || []
args.global.plugins.push(createPinia())
args.global.plugins.push(createI18n())

return mount(
() => {
return h(VApp, {}, component)
},
...args
)
})

复制预期的组件层次结构

一些 Vue 应用程序,最著名的是基于 Vuetify 构建的 Vue 应用,要求某些组件以特定的层次结构构建。

所有 Vuetify 应用程序都要求你在构建应用时将应用包装在 VApp 组件中。这是 Vuetify 的实现细节,但一旦用户尝试测试依赖 Vuetify 的组件,他们会遇到 Vuetify 特定的编译错误,并很快发现他们需要复制该组件层次结构,任何时候他们需要挂载使用 Vuetify 组件的组件!

自定义 cy.mount 命令来拯救!你可能会发现 JSX 语法更直接。

你还需要按照 Vuetify 文档复制插件设置步骤,以便一切编译。

import Vuetify from 'vuetify/lib'
import { VApp } from 'vuetify'
import { mount } from 'cypress/vue'
import { h } from 'vue'

// 我们建议你将这个提取出来
// 到一个与你的 main.js 文件共享的常量文件中。
const vuetifyOptions = {}

Cypress.Commands.add('mount', (component, ...args) => {
args.global = args.global || {}
args.global.plugins = args.global.plugins || []
args.global.plugins.push(new Vuetify(vuetifyOptions))

return mount(
() => {
return h(VApp, {}, component)
},
...args
)
})

Vue Router

要使用 Vue Router,创建一个命令来注册插件并通过 options 参数传入路由器的自定义实现。

import { mount } from 'cypress/vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import { routes } from '../../src/router'

Cypress.Commands.add('mount', (component, options = {}) => {
// 设置 options 对象
options.global = options.global || {}
options.global.plugins = options.global.plugins || []

// 如果没有提供 router,则创建一个
if (!options.router) {
options.router = createRouter({
routes: routes,
history: createMemoryHistory(),
})
}

// 添加 router 插件
options.global.plugins.push({
install(app) {
app.use(options.router)
},
})

return mount(component, options)
})

Vuex

要使用依赖 Vuex 的组件,创建一个 mount 命令来为你的组件配置 Vuex store。

import { mount } from 'cypress/vue'
import { getStore } from '../../src/plugins/store'

Cypress.Commands.add('mount', (component, options = {}) => {
// 设置 options 对象
options.global = options.global || {}
options.global.stubs = options.global.stubs || {}
options.global.stubs['transition'] = false
options.global.components = options.global.components || {}
options.global.plugins = options.global.plugins || []

// 使用 options 中传入的 store,或初始化一个新的
const { store = getStore(), ...mountOptions } = options

// 添加 Vuex 插件
options.global.plugins.push({
install(app) {
app.use(store)
},
})

return mount(component, mountOptions)
})
info

getStore 方法是一个工厂方法,用于初始化 Vuex 并创建一个新的 store。重要的是每个新测试都要初始化 store,以确保 store 的更改不会影响其他测试。

全局组件

如果你有在主应用文件中全局注册的组件,在你的挂载命令中设置它们,以便你的组件能正确渲染它们:

import { mount } from 'cypress/vue'
import Button from '../../src/components/Button.vue'

Cypress.Commands.add('mount', (component, options = {}) => {
// 设置 options 对象
options.global = options.global || {}
options.global.components = options.global.components || {}

// 注册全局组件
options.global.components['Button'] = Button

return mount(component, options)
})