Vue 示例
你将学习到
- 如何在 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)
})
使用插槽
默认插槽
- DefaultSlot.cy.js
- DefaultSlot.cy.jsx (JSX)
- DefaultSlot.vue
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 DefaultSlot from './DefaultSlot.vue'
describe('<DefaultSlot />', () => {
it('renders', () => {
cy.mount(<DefaultSlot>Hello there!</DefaultSlot>)
cy.get('div.content').should('have.text', 'Hello there!')
})
})
<template>
<div>
<div class="content">
<slot />
</div>
</div>
</template>
<script setup></script>
具名插槽
- NamedSlot.cy.js
- NamedSlot.cy.jsx (JSX)
- NamedSlot.vue
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')
})
})
import NamedSlot from './NamedSlot.vue'
describe('<NamedSlot />', () => {
it('renders', () => {
const slots = {
header: 'my header',
footer: 'my footer',
}
cy.mount(<NamedSlot>{{ ...slots }}</NamedSlot>)
cy.get('header').should('have.text', 'my header')
cy.get('footer').should('have.text', 'my footer')
})
})
<template>
<div>
<header>
<slot name="header" />
</header>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
<script setup></script>
有关使用插槽测试 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.called
和 to.have.been.calledWith
等方法。
使用 cy.get('@vue')
别名的代码可能如下所示。
注意我们使用 'should'
函数签名来利用 Cypress 的 重试机制。如果我们使用 cy.then
而不是 cy.should
进行链式调用,可能会遇到 Vue Test Utils 测试中的那种问题,即你需要频繁使用 await
来确保 DOM 已更新或任何响应式事件已触发。
- With emitted
- With spies
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')
})
const onChangeSpy = cy.spy().as('onChangeSpy')
cy.mount(Stepper, { props: { initial: 100, onChange: onChangeSpy } })
cy.get(incrementSelector).click()
cy.get('@onChangeSpy').should('have.been.calledWith', '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 插件。
- cypress/support/component.js
- With JSX
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
)
})
import { createPinia } from 'pinia' // 或 Vuex
import { createI18n } from 'vue-i18n'
import { mount } from 'cypress/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())
// <component> 是 Vue 内置的组件
return mount(
() => (
<VApp>
<component is={component} />
</VApp>
),
...args
)
})
复制预期的组件层次结构
一些 Vue 应用程序,最著名的是基于 Vuetify 构建的 Vue 应用,要求某些组件以特定的层次结构构建。
所有 Vuetify 应用程序都要求你在构建应用时将应用包装在 VApp
组件中。这是 Vuetify 的实现细节,但一旦用户尝试测试依赖 Vuetify 的组件,他们会遇到 Vuetify 特定的编译错误,并很快发现他们需要复制该组件层次结构,任何时候他们需要挂载使用 Vuetify 组件的组件!
自定义 cy.mount
命令来拯救!你可能会发现 JSX 语法更直接。
你还需要按照 Vuetify 文档复制插件设置步骤,以便一切编译。
- cypress/support/component.js
- With JSX
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
)
})
import Vuetify from 'vuetify/lib'
import { VApp } from 'vuetify'
import { mount } from 'cypress/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))
// <component> 是 Vue 内置的组件
return mount(
() => (
<VApp>
<component is={component} />
</VApp>
),
...args
)
})
Vue Router
要使用 Vue Router,创建一个命令来注册插件并通过 options 参数传入路由器的自定义实现。
- cypress/support/component.js
- Typings
- Spec Usage
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)
})
import { mount } from 'cypress/vue'
import { Router } from 'vue-router'
type MountParams = Parameters<typeof mount>
type OptionsParam = MountParams[1] & { router?: Router }
declare global {
namespace Cypress {
interface Chainable {
/**
* Vue 组件的辅助挂载函数
* @param component 要挂载的 Vue 组件或 JSX 元素
* @param options 传递给 Vue Test Utils 的选项
*/
mount(component: any, options?: OptionsParam): Chainable<any>
}
}
}
import Navigation from './Navigation.vue'
import { routes } from '../router'
import { createMemoryHistory, createRouter } from 'vue-router'
it('home link should be active when url is "/"', () => {
// 无需传入自定义 router,因为默认 url 是 '/'
cy.mount(<Navigation />)
cy.get('a').contains('Home').should('have.class', 'router-link-active')
})
it('login link should be active when url is "/login"', () => {
// 为每个测试创建一个新的 router 实例
const router = createRouter({
routes: routes,
history: createMemoryHistory(),
})
// 将位置更改为 `/login`,
// 并使用 cy.wrap 等待 promise
cy.wrap(router.push('/login'))
// 传递已初始化的 router 供使用
cy.mount(<Navigation />, { router })
cy.get('a').contains('Login').should('have.class', 'router-link-active')
})
在 Vue 3 的路由器中调用 router.push()
是一个异步操作。使用 cy.wrap 命令让 Cypress 在继续其他命令之前等待 promise 的 resolve:
Vuex
要使用依赖 Vuex 的组件,创建一个 mount
命令来为你的组件配置 Vuex store。
- cypress/support/component.js
- Typings
- Spec Usage
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)
})
getStore
方法是一个工厂方法,用于初始化 Vuex 并创建一个新的 store。重要的是每个新测试都要初始化 store,以确保 store 的更改不会影响其他测试。
import { mount } from 'cypress/vue'
import { Store } from 'vuex'
type MountParams = Parameters<typeof mount>
type OptionsParam = MountParams[1]
declare global {
namespace Cypress {
interface Chainable {
/**
* Vue 组件的辅助挂载函数
* @param component 要挂载的 Vue 组件或 JSX 元素
* @param options 传递给 Vue Test Utils 的选项
*/
mount(
component: any,
options?: OptionsParam & { store?: Store }
): Chainable<any>
}
}
}
import { getStore } from '@/plugins/store'
import UserProfile from './UserProfile.vue'
it.only('User profile should display user name', () => {
const user = { name: 'test person' }
// getStore 是一个创建新 store 的工厂方法
const store = getStore()
// 用 user 修改 store
store.commit('setUser', user)
cy.mount(UserProfile, {
store,
})
cy.get('div.name').should('have.text', user.name)
})
全局组件
如果你有在主应用文件中全局注册的组 件,在你的挂载命令中设置它们,以便你的组件能正确渲染它们:
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)
})