From 4b22b671ee68c6823dc5112b5084972ffb966a23 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 12:10:14 -0400 Subject: [PATCH 1/7] feat: export TS types --- package.json | 1 + src/{index.js => index.ts} | 160 ++++++++++++++++++++++-------------- src/preprocessor/webpack.js | 6 +- tsconfig.json | 5 +- 4 files changed, 105 insertions(+), 67 deletions(-) rename src/{index.js => index.ts} (54%) diff --git a/package.json b/package.json index 1c9c816..55099c3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ ], "license": "MIT", "main": "dist/index.js", + "types": "dist", "publishConfig": { "registry": "http://registry.npmjs.org/" }, diff --git a/src/index.js b/src/index.ts similarity index 54% rename from src/index.js rename to src/index.ts index 963ebd7..a7e1787 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,6 +1,6 @@ /// -const Vue = require('vue').default +import Vue from 'vue' const { stripIndent } = require('common-tags') // mountVue options @@ -71,9 +71,10 @@ const installMixins = (Vue, options) => { const isConstructor = (object) => object && object._compiled -const hasStore = ({ store }) => store && store._vm +// @ts-ignore +const hasStore = ({ store }: { store: object }) => store && store._vm -const forEachValue = (obj, fn) => +const forEachValue = (obj: object, fn: Function) => Object.keys(obj).forEach((key) => fn(obj[key], key)) const resetStoreVM = (Vue, { store }) => { @@ -100,13 +101,27 @@ const resetStoreVM = (Vue, { store }) => { return store } -const mountVue = (component, optionsOrProps = {}) => { +/** + * 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-unit-test' + * 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: object, optionsOrProps = {}) => { checkMountModeEnabled() const options = Cypress._.pick(optionsOrProps, defaultOptions) const props = Cypress._.omit(optionsOrProps, defaultOptions) // display deprecation warnings + // @ts-ignore if (options.vue) { console.warn(stripIndent` [DEPRECATION]: 'vue' option has been deprecated. @@ -120,7 +135,9 @@ const mountVue = (component, optionsOrProps = {}) => { // appIframe.contentWindow.Vue = Vue // refresh inner Vue instance of Vuex store + // @ts-ignore if (hasStore(component)) { + // @ts-ignore component.store = resetStoreVM(Vue, component) } @@ -128,68 +145,87 @@ const mountVue = (component, optionsOrProps = {}) => { // https://github.com/bahmutov/cypress-vue-unit-test/issues/313 if ( Cypress._.isPlainObject(component) && + // @ts-ignore Cypress._.isFunction(component.render) ) { + // @ts-ignore component._compiled = true } - return cy.window({ log: false }).then((win) => { - win.Vue = Vue - - const document = cy.state('document') - let el = document.getElementById('cypress-jsdom') - - // If the target div doesn't exist, create it - if (!el) { - const div = document.createElement('div') - div.id = 'cypress-jsdom' - document.body.appendChild(div) - el = div - } - - if (typeof options.stylesheets === 'string') { - options.stylesheets = [options.stylesheets] - } - if (Array.isArray(options.stylesheets)) { - console.log('adding 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(Vue, options) - installMixins(Vue, options) - installPlugins(Vue, options) - registerGlobalComponents(Vue, options) - deleteCachedConstructors(component) - - // create root Vue component - // and make it accessible via Cypress.vue - if (isConstructor(component)) { - const Cmp = Vue.extend(component) - Cypress.vue = new Cmp(props).$mount(componentNode) - } else { - Cypress.vue = new Vue(component).$mount(componentNode) - } - }) -} + return cy + .window({ + log: false, + }) + .then((win) => { + // @ts-ignore + win.Vue = Vue + + // @ts-ignore + const document = cy.state('document') + let el = document.getElementById('cypress-jsdom') + + // If the target div doesn't exist, create it + if (!el) { + const div = document.createElement('div') + div.id = 'cypress-jsdom' + document.body.appendChild(div) + el = div + } -// the double function allows mounting a component quickly -// beforeEach(mountVue(component, options)) -const mountCallback = (...args) => () => mountVue(...args) + // @ts-ignore + if (typeof options.stylesheets === 'string') { + // @ts-ignore + options.stylesheets = [options.stylesheets] + } + // @ts-ignore + if (Array.isArray(options.stylesheets)) { + // console.log('adding stylesheets') + // @ts-ignore + options.stylesheets.forEach((href) => { + const link = document.createElement('link') + link.type = 'text/css' + link.rel = 'stylesheet' + link.href = href + el.append(link) + }) + } + + // @ts-ignore + if (options.style) { + const style = document.createElement('style') + // @ts-ignore + style.appendChild(document.createTextNode(options.style)) + el.append(style) + } + + const componentNode = document.createElement('div') + el.append(componentNode) + + // setup Vue instance + installFilters(Vue, options) + installMixins(Vue, options) + installPlugins(Vue, options) + registerGlobalComponents(Vue, options) + deleteCachedConstructors(component) + + // create root Vue component + // and make it accessible via Cypress.vue + if (isConstructor(component)) { + const Cmp = Vue.extend(component) + // @ts-ignore + Cypress.vue = new Cmp(props).$mount(componentNode) + } else { + // @ts-ignore + Cypress.vue = new Vue(component).$mount(componentNode) + } + }) +} -module.exports = { mount: mountVue, mountCallback } +/** + * Helper function for mounting a component quickly in test hooks. + * @example + * import {mountCallback} from 'cypress-vue-unit-test' + * beforeEach(mountVue(component, options)) + */ +export const mountCallback = (component: object, options?: object) => () => + mount(component, options) diff --git a/src/preprocessor/webpack.js b/src/preprocessor/webpack.js index 04e986e..4f09c16 100644 --- a/src/preprocessor/webpack.js +++ b/src/preprocessor/webpack.js @@ -1,5 +1,5 @@ -const webpack = require('webpack') -const util = require('util') +import webpack from 'webpack' +import util from 'util' // Cypress webpack bundler adaptor // https://github.com/cypress-io/cypress-webpack-preprocessor @@ -54,7 +54,7 @@ function compileTemplate(options = {}) { /** * Warning: modifies the input object * @param {Cypress.ConfigOptions} config - * @param {import('webpack/lib/Compiler').WebpackOptions} options + * @param {WebpackOptions} options */ function insertBabelLoader(config, options) { const skipCodeCoverage = config && config.env && config.env.coverage === false diff --git a/tsconfig.json b/tsconfig.json index 19d647e..edf0d1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declaration": true /* Generates corresponding '.d.ts' file. */, // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "dist" /* Redirect output structure to the directory. */, @@ -23,7 +23,8 @@ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, + "strict": false /* Enable all strict type-checking options. */, + "noImplicitAny": false, /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ From 22da7ee1b2b9a2eed09ded36b13572be748fbda2 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 14:54:37 -0400 Subject: [PATCH 2/7] start interfaces for mount type --- src/index.ts | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index a7e1787..8643afa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,31 @@ const resetStoreVM = (Vue, { store }) => { return store } +/** + * Options to pass to the component when creating it, like + * props. + * + * @interface ComponentOptions + */ +interface ComponentOptions {} + +/** + * 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 + * @type {unknown} + * @memberof MountOptions + */ + vue: unknown +} + /** * Mounts a Vue component inside Cypress browser. * @param {object} component imported from Vue file @@ -114,14 +139,22 @@ const resetStoreVM = (Vue, { store }) => { * cy.get('#greeting').should('be.visible') * }) */ -export const mount = (component: object, optionsOrProps = {}) => { +export const mount = ( + component: object, + optionsOrProps: Partial = {}, +) => { checkMountModeEnabled() - const options = Cypress._.pick(optionsOrProps, defaultOptions) - const props = Cypress._.omit(optionsOrProps, defaultOptions) + const options: Partial = Cypress._.pick( + optionsOrProps, + defaultOptions, + ) + const props: Partial = Cypress._.omit( + optionsOrProps, + defaultOptions, + ) // display deprecation warnings - // @ts-ignore if (options.vue) { console.warn(stripIndent` [DEPRECATION]: 'vue' option has been deprecated. From 98686c13993811355756f837658579d511875a2b Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 15:08:34 -0400 Subject: [PATCH 3/7] more types --- src/index.ts | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8643afa..f31df75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,17 @@ const resetStoreVM = (Vue, { store }) => { return store } +/** + * Type for component passed to "mount" + * + * @interface VueComponent + * @example + * import Hello from './Hello.vue' + * ^^^^^ this type + * mount(Hello) + */ +type VueComponent = Vue.ComponentOptions + /** * Options to pass to the component when creating it, like * props. @@ -124,8 +135,28 @@ interface MountOptions { * @memberof MountOptions */ vue: unknown + + /** + * CSS style string to inject when mounting the component + * + * @type {string} + * @memberof MountOptions + * @example + * const style = ` + * .todo.done { + * text-decoration: line-through; + * color: gray; + * }` + * mount(Todo, { style }) + */ + style: string } +/** + * Utility type for union of options passed to "mount(..., options)" + */ +type MountOptionsArgument = Partial + /** * Mounts a Vue component inside Cypress browser. * @param {object} component imported from Vue file @@ -140,8 +171,8 @@ interface MountOptions { * }) */ export const mount = ( - component: object, - optionsOrProps: Partial = {}, + component: VueComponent, + optionsOrProps: MountOptionsArgument = {}, ) => { checkMountModeEnabled() @@ -178,7 +209,6 @@ export const mount = ( // https://github.com/bahmutov/cypress-vue-unit-test/issues/313 if ( Cypress._.isPlainObject(component) && - // @ts-ignore Cypress._.isFunction(component.render) ) { // @ts-ignore @@ -194,7 +224,7 @@ export const mount = ( win.Vue = Vue // @ts-ignore - const document = cy.state('document') + const document: Document = cy.state('document') let el = document.getElementById('cypress-jsdom') // If the target div doesn't exist, create it @@ -223,10 +253,8 @@ export const mount = ( }) } - // @ts-ignore if (options.style) { const style = document.createElement('style') - // @ts-ignore style.appendChild(document.createTextNode(options.style)) el.append(style) } @@ -260,5 +288,7 @@ export const mount = ( * import {mountCallback} from 'cypress-vue-unit-test' * beforeEach(mountVue(component, options)) */ -export const mountCallback = (component: object, options?: object) => () => - mount(component, options) +export const mountCallback = ( + component: VueComponent, + options?: MountOptionsArgument, +) => () => mount(component, options) From 5731b7ca112ce4b615f67348a160a21f433adaf2 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 15:14:53 -0400 Subject: [PATCH 4/7] more types --- src/index.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index f31df75..ebd4fde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,7 +139,6 @@ interface MountOptions { /** * CSS style string to inject when mounting the component * - * @type {string} * @memberof MountOptions * @example * const style = ` @@ -150,6 +149,23 @@ interface MountOptions { * mount(Todo, { style }) */ style: string + + /** + * Stylesheet(s) urls to inject as `` 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[] } /** @@ -235,15 +251,10 @@ export const mount = ( el = div } - // @ts-ignore if (typeof options.stylesheets === 'string') { - // @ts-ignore options.stylesheets = [options.stylesheets] } - // @ts-ignore if (Array.isArray(options.stylesheets)) { - // console.log('adding stylesheets') - // @ts-ignore options.stylesheets.forEach((href) => { const link = document.createElement('link') link.type = 'text/css' From ef4b9f0e120f8ab6bfa718f6a21c51e3c57fa059 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 15:23:21 -0400 Subject: [PATCH 5/7] document extensions property --- src/index.ts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index ebd4fde..a52ba52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,6 +120,33 @@ type VueComponent = Vue.ComponentOptions */ interface ComponentOptions {} +/** + * Additional Vue services to register while mounting the component, like + * local components, plugins, etc. + * + * @interface MountOptionsExtensions + * @see https://github.com/bahmutov/cypress-vue-unit-test#examples + */ +interface MountOptionsExtensions { + /** + * Extra local components + * + * @memberof MountOptionsExtensions + * @see https://github.com/bahmutov/cypress-vue-unit-test#examples + * @example + * import Hello from './Hello.vue' + * // imagine Hello needs AppComponent + * // that it uses in its template like + * // during testing we can replace it with a mock component + * const appComponent = ... + * const components = { + * 'app-component': appComponent + * }, + * mount(Hello, { extensions: { components }}) + */ + components?: object +} + /** * Options controlling how the component is going to be mounted, * including global Vue plugins and extensions. @@ -131,7 +158,6 @@ interface MountOptions { * Vue instance to use. * * @deprecated - * @type {unknown} * @memberof MountOptions */ vue: unknown @@ -166,6 +192,15 @@ interface MountOptions { * mount({ template }, { stylesheets }) */ stylesheets: string | string[] + + /** + * Extra Vue plugins, mixins, local components to register while + * mounting this component + * + * @memberof MountOptions + * @see https://github.com/bahmutov/cypress-vue-unit-test#examples + */ + extensions: MountOptionsExtensions } /** From 970d37a191e2b7bf2e51ffcfbe80f4041d205159 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 15:37:12 -0400 Subject: [PATCH 6/7] add mixins --- src/index.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index a52ba52..9ec7f7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,12 @@ import Vue from 'vue' const { stripIndent } = require('common-tags') // mountVue options -const defaultOptions = ['vue', 'extensions', 'style', 'stylesheets'] +const defaultOptions: (keyof MountOptions)[] = [ + 'vue', + 'extensions', + 'style', + 'stylesheets', +] function checkMountModeEnabled() { // @ts-ignore @@ -34,7 +39,10 @@ const registerGlobalComponents = (Vue, options) => { } const installFilters = (Vue, options) => { - const filters = Cypress._.get(options, 'extensions.filters') + const filters: VueFilters | undefined = Cypress._.get( + options, + 'extensions.filters', + ) if (Cypress._.isPlainObject(filters)) { Object.keys(filters).forEach((name) => { Vue.filter(name, filters[name]) @@ -120,6 +128,16 @@ type VueComponent = Vue.ComponentOptions */ interface ComponentOptions {} +// local placeholder types +type VueLocalComponents = object + +type VueFilters = { + [key: string]: Function +} + +type VueMixin = unknown +type VueMixins = VueMixin | VueMixin[] + /** * Additional Vue services to register while mounting the component, like * local components, plugins, etc. @@ -144,7 +162,38 @@ interface MountOptionsExtensions { * }, * mount(Hello, { extensions: { components }}) */ - components?: object + components?: VueLocalComponents + + /** + * Optional Vue filters to install while mounting the component + * + * @memberof MountOptionsExtensions + * @see https://github.com/bahmutov/cypress-vue-unit-test#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/bahmutov/cypress-vue-unit-test#examples + */ + mixin?: VueMixins + + /** + * Optional Vue mixin(s) to install when mounting the component + * + * @memberof MountOptionsExtensions + * @alias mixin + * @see https://github.com/bahmutov/cypress-vue-unit-test#examples + */ + mixins?: VueMixins } /** From 37c5c3d277defa2cc121dcd3227f0ad835614e40 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Wed, 8 Jul 2020 15:44:49 -0400 Subject: [PATCH 7/7] add types for plugins when mounting --- src/index.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 9ec7f7c..7bdad12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,9 +51,10 @@ const installFilters = (Vue, options) => { } const installPlugins = (Vue, options) => { - const plugins = + const plugins: VuePlugins = Cypress._.get(options, 'extensions.use') || Cypress._.get(options, 'extensions.plugins') + if (Cypress._.isArray(plugins)) { plugins.forEach((plugin) => { if (Array.isArray(plugin)) { @@ -138,6 +139,16 @@ type VueFilters = { type VueMixin = unknown type VueMixins = VueMixin | VueMixin[] +/** + * A Vue plugin to register, + * or a plugin + its options pair inside an array + */ +type VuePlugin = unknown | [unknown, unknown] +/** + * A single Vue plugin or a list of plugins to register + */ +type VuePlugins = VuePlugin | VuePlugin[] + /** * Additional Vue services to register while mounting the component, like * local components, plugins, etc. @@ -194,6 +205,24 @@ interface MountOptionsExtensions { * @see https://github.com/bahmutov/cypress-vue-unit-test#examples */ mixins?: VueMixins + + /** + * A single plugin or multiple plugins. + * + * @see https://github.com/bahmutov/cypress-vue-unit-test#examples + * @alias plugins + * @memberof MountOptionsExtensions + */ + use?: VuePlugins + + /** + * A single plugin or multiple plugins. + * + * @see https://github.com/bahmutov/cypress-vue-unit-test#examples + * @alias use + * @memberof MountOptionsExtensions + */ + plugins?: VuePlugins } /**