@revoloo/cypress6
Version:
Cypress.io end to end testing tool
425 lines (369 loc) • 10.8 kB
text/typescript
/// <reference types="cypress" />
import Vue from 'vue'
import {
createLocalVue,
mount as testUtilsMount,
VueTestUtilsConfigOptions,
Wrapper,
} from '@vue/test-utils'
import { renderTestingPlatform, ROOT_ID } from './renderTestingPlatform'
const defaultOptions: (keyof MountOptions)[] = [
'vue',
'extensions',
'style',
'stylesheets',
]
const registerGlobalComponents = (Vue, options) => {
const globalComponents = Cypress._.get(options, 'extensions.components')
if (Cypress._.isPlainObject(globalComponents)) {
Cypress._.forEach(globalComponents, (component, id) => {
Vue.component(id, component)
})
}
}
const installFilters = (Vue, options) => {
const filters: VueFilters | undefined = Cypress._.get(
options,
'extensions.filters',
)
if (Cypress._.isPlainObject(filters)) {
Object.keys(filters).forEach((name) => {
Vue.filter(name, filters[name])
})
}
}
const installPlugins = (Vue, options, props) => {
const plugins: VuePlugins =
Cypress._.get(props, 'plugins') ||
Cypress._.get(options, 'extensions.use') ||
Cypress._.get(options, 'extensions.plugins') ||
[]
// @ts-ignore
plugins.forEach((p) => {
Array.isArray(p) ? Vue.use(...p) : Vue.use(p)
})
}
const installMixins = (Vue, options) => {
const mixins =
Cypress._.get(options, 'extensions.mixin') ||
Cypress._.get(options, 'extensions.mixins')
if (Cypress._.isArray(mixins)) {
mixins.forEach((mixin) => {
Vue.mixin(mixin)
})
}
}
const hasStore = ({ store }: { store: any }) => store && store._vm // @ts-ignore
const forEachValue = <T>(obj: Record<string, T>, fn: (value: T, key: string) => void) => {
return Object.keys(obj).forEach((key) => fn(obj[key], key))
}
const resetStoreVM = (Vue, { store }) => {
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters as Record<string, (store: any) => void>
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true, // for local getters
})
})
store._watcherVM = new Vue()
store._vm = new Vue({
data: {
$$state: store._vm._data.$$state,
},
computed,
})
return store
}
/**
* Type for component passed to "mount"
*
* @interface VueComponent
* @example
* import Hello from './Hello.vue'
* ^^^^^ this type
* mount(Hello)
*/
type VueComponent = Vue.ComponentOptions<any> | Vue.VueConstructor
/**
* Options to pass to the component when creating it, like
* props.
*
* @interface ComponentOptions
*/
type ComponentOptions = Record<string, unknown>
// local placeholder types
type VueLocalComponents = Record<string, VueComponent>
type VueFilters = {
[key: string]: (value: string) => string
}
type VueMixin = unknown
type VueMixins = VueMixin | VueMixin[]
type VuePluginOptions = unknown
type VuePlugin = unknown | [unknown, VuePluginOptions]
/**
* A single Vue plugin or a list of plugins to register
*/
type VuePlugins = VuePlugin[]
/**
* Additional Vue services to register while mounting the component, like
* local components, plugins, etc.
*
* @interface MountOptionsExtensions
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
*/
interface MountOptionsExtensions {
/**
* Extra local components
*
* @memberof MountOptionsExtensions
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
* @example
* import Hello from './Hello.vue'
* // imagine Hello needs AppComponent
* // that it uses in its template like <app-component ... />
* // during testing we can replace it with a mock component
* const appComponent = ...
* const components = {
* 'app-component': appComponent
* },
* mount(Hello, { extensions: { components }})
*/
components?: VueLocalComponents
/**
* Optional Vue filters to install while mounting the component
*
* @memberof MountOptionsExtensions
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
* @example
* const filters = {
* reverse: (s) => s.split('').reverse().join(''),
* }
* mount(Hello, { extensions: { filters }})
*/
filters?: VueFilters
/**
* Optional Vue mixin(s) to install when mounting the component
*
* @memberof MountOptionsExtensions
* @alias mixins
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
*/
mixin?: VueMixins
/**
* Optional Vue mixin(s) to install when mounting the component
*
* @memberof MountOptionsExtensions
* @alias mixin
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
*/
mixins?: VueMixins
/**
* A single plugin or multiple plugins.
*
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
* @alias plugins
* @memberof MountOptionsExtensions
*/
use?: VuePlugins
/**
* A single plugin or multiple plugins.
*
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
* @alias use
* @memberof MountOptionsExtensions
*/
plugins?: VuePlugins
}
/**
* Options controlling how the component is going to be mounted,
* including global Vue plugins and extensions.
*
* @interface MountOptions
*/
interface MountOptions {
/**
* Vue instance to use.
*
* @deprecated
* @memberof MountOptions
*/
vue: unknown
/**
* CSS style string to inject when mounting the component
*
* @memberof MountOptions
* @example
* const style = `
* .todo.done {
* text-decoration: line-through;
* color: gray;
* }`
* mount(Todo, { style })
*/
style: string
/**
* Stylesheet(s) urls to inject as `<link ... />` elements when
* mounting the component
*
* @memberof MountOptions
* @example
* const template = '...'
* const stylesheets = '/node_modules/tailwindcss/dist/tailwind.min.css'
* mount({ template }, { stylesheets })
*
* @example
* const template = '...'
* const stylesheets = ['https://cdn.../lib.css', 'https://lib2.css']
* mount({ template }, { stylesheets })
*/
stylesheets: string | string[]
/**
* Extra Vue plugins, mixins, local components to register while
* mounting this component
*
* @memberof MountOptions
* @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
*/
extensions: MountOptionsExtensions
}
/**
* Utility type for union of options passed to "mount(..., options)"
*/
type MountOptionsArgument = Partial<ComponentOptions & MountOptions & VueTestUtilsConfigOptions>
// when we mount a Vue component, we add it to the global Cypress object
// so here we extend the global Cypress namespace and its Cypress interface
declare global {
// eslint-disable-next-line no-redeclare
namespace Cypress {
interface Cypress {
/**
* Mounted Vue instance is available under Cypress.vue
* @memberof Cypress
* @example
* mount(Greeting)
* .then(() => {
* Cypress.vue.message = 'Hello There'
* })
* // new message is displayed
* cy.contains('Hello There').should('be.visible')
*/
vue: Vue
vueWrapper: Wrapper<Vue>
}
}
}
/**
* Direct Vue errors to the top error handler
* where they will fail Cypress test
* @see https://vuejs.org/v2/api/#errorHandler
* @see https://github.com/cypress-io/cypress/issues/7910
*/
function failTestOnVueError (err, vm, info) {
console.error(`Vue error`)
console.error(err)
console.error('component:', vm)
console.error('info:', info)
window.top.onerror(err)
}
/**
* Mounts a Vue component inside Cypress browser.
* @param {object} component imported from Vue file
* @example
* import Greeting from './Greeting.vue'
* import { mount } from '@cypress/vue'
* it('works', () => {
* // pass props, additional extensions, etc
* mount(Greeting, { ... })
* // use any Cypress command to test the component
* cy.get('#greeting').should('be.visible')
* })
*/
export const mount = (
component: VueComponent,
optionsOrProps: MountOptionsArgument = {},
) => {
const options: Partial<MountOptions> = Cypress._.pick(
optionsOrProps,
defaultOptions,
)
const props: Partial<ComponentOptions> = Cypress._.omit(
optionsOrProps,
defaultOptions,
)
return cy
.window({
log: false,
})
.then((win) => {
const localVue = createLocalVue()
// @ts-ignore
win.Vue = localVue
localVue.config.errorHandler = failTestOnVueError
// set global Vue instance:
// 1. convenience for debugging in DevTools
// 2. some libraries might check for this global
// appIframe.contentWindow.Vue = localVue
// refresh inner Vue instance of Vuex store
// @ts-ignore
if (hasStore(component)) {
// @ts-ignore
component.store = resetStoreVM(localVue, component)
}
// @ts-ignore
const document: Document = cy.state('document')
document.body.innerHTML = ''
let el = document.getElementById(ROOT_ID)
// If the target div doesn't exist, create it
if (!el) {
el = renderTestingPlatform(document.head.innerHTML)
}
if (typeof options.stylesheets === 'string') {
options.stylesheets = [options.stylesheets]
}
if (Array.isArray(options.stylesheets)) {
options.stylesheets.forEach((href) => {
const link = document.createElement('link')
link.type = 'text/css'
link.rel = 'stylesheet'
link.href = href
el.append(link)
})
}
if (options.style) {
const style = document.createElement('style')
style.appendChild(document.createTextNode(options.style))
el.append(style)
}
const componentNode = document.createElement('div')
el.append(componentNode)
// setup Vue instance
installFilters(localVue, options)
installMixins(localVue, options)
// @ts-ignore
installPlugins(localVue, options, props)
registerGlobalComponents(localVue, options)
// @ts-ignore
props.attachTo = componentNode
const wrapper = localVue.extend(component as any)
const VTUWrapper = testUtilsMount(wrapper, { localVue, ...props })
Cypress.vue = VTUWrapper.vm
Cypress.vueWrapper = VTUWrapper
})
}
/**
* Helper function for mounting a component quickly in test hooks.
* @example
* import {mountCallback} from '@cypress/vue'
* beforeEach(mountVue(component, options))
*/
export const mountCallback = (
component: VueComponent,
options?: MountOptionsArgument,
) => {
return () => mount(component, options)
}