UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

425 lines (369 loc) 10.8 kB
/// <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) }