diff --git a/README.md b/README.md index 8c4b04e..4b39824 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,11 @@ See examples below for details. See [cypress/integration/options-spec.js](cypress/integration/options-spec.js) for examples of options. -* `vue` - path or URL to the Vue library to load. By default, will -try to load `../node_modules/vue/dist/vue.js`, but you can pass your -own path or URL. +* `mountId` - specify root Vue app mount element ID. Defaults to `app`. ```js const options = { - vue: 'https://unpkg.com/vue' + mountId: 'rootApp' // div#rootApp } beforeEach(mountVue(/* my Vue code */, options)) ``` @@ -71,12 +69,25 @@ beforeEach(mountVue(/* my Vue code */, options)) place to load additional libraries, polyfills and styles. ```js -const vue = '../node_modules/vue/dist/vue.js' +const polyfill = '../node_modules/mypolyfill/dist/polyfill.js' const options = { - html: `
` + html: `
` +} +beforeEach(mountVue(/* my Vue code */, options)) +``` + +* `vue` **[DEPRECATED]** - path or URL to the Vue library to load. By default, will +try to load `../node_modules/vue/dist/vue.js`, but you can pass your +own path or URL. + +```js +const options = { + vue: 'https://unpkg.com/vue' } beforeEach(mountVue(/* my Vue code */, options)) ``` +> #### Deprecation Warning +> `vue` option has been deprecated. `node_modules/vue/dist/vue` is always used. ### Global Vue extensions diff --git a/components/.babelrc b/components/.babelrc new file mode 100644 index 0000000..02e7f24 --- /dev/null +++ b/components/.babelrc @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-object-rest-spread" + ] +} \ No newline at end of file diff --git a/components/Counter.vue b/components/Counter.vue index bc353be..c8b6ae2 100644 --- a/components/Counter.vue +++ b/components/Counter.vue @@ -1,24 +1,31 @@ diff --git a/components/store.js b/components/store.js index e001983..468106b 100644 --- a/components/store.js +++ b/components/store.js @@ -14,6 +14,9 @@ const state = { // mutations must be synchronous and can be recorded by plugins // for debugging purposes. const mutations = { + set (state, value) { + state.count = value + }, increment (state) { state.count++ }, diff --git a/cypress.json b/cypress.json index 34d0e84..a3ea8df 100644 --- a/cypress.json +++ b/cypress.json @@ -1,6 +1,6 @@ { "viewportWidth": 300, - "viewportHeight": 100, + "viewportHeight": 120, "videoRecording": false, "projectId": "134ej7" } diff --git a/cypress/integration/counter-vuex-spec.js b/cypress/integration/counter-vuex-spec.js index 51228e4..76926c1 100644 --- a/cypress/integration/counter-vuex-spec.js +++ b/cypress/integration/counter-vuex-spec.js @@ -7,20 +7,24 @@ import mountVue from '../..' /* eslint-env mocha */ describe('Vuex Counter', () => { + + // configure component const extensions = { plugins: [Vuex], components: { - counter: Counter - }, + Counter + } } + + // define component template const template = '' - beforeEach(mountVue({template, store}, {extensions})) - const getCount = () => - Cypress.vue.$store.state.count + // define count get and set helpers + const getCount = () => Cypress.vue.$store.state.count + const setCount = value => Cypress.vue.$store.commit('set', value) - const setCount = value => - Cypress.vue.$set(Cypress.vue.$store.state, 'count', value) + // initialize a fresh Vue app before each test + beforeEach(mountVue({template, store}, {extensions})) it('starts with zero', () => { cy.contains('0 times') @@ -37,15 +41,30 @@ describe('Vuex Counter', () => { }) it('increments the counter if count is odd', () => { - setCount(3) + setCount(3) // start with an odd number cy.contains('odd') - cy.contains('button', 'Increment if odd').click() + cy.contains('button', 'Increment if odd').as('btn').click() + cy.contains('even') + cy.get('@btn').click() cy.contains('even') }) it('asynchronously increments counter', () => { const count = getCount() + // increment mutation is delayed by 1 second + // Cypress waits 4 seconds by default cy.contains('button', 'Increment async').click() cy.contains(`${count + 1} times`) }) + + it('count is zero when input is cleared', () => { + cy.get('input').type(`{selectall}{backspace}`) + cy.contains('0 times') + }), + + it('set count via input field', () => { + const count = 42 + cy.get('input').type(`{selectall}{backspace}${count}`) + cy.contains(`${count} times`) + }) }) diff --git a/package.json b/package.json index f0f8ed6..35f703a 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "generateNotes": "github-post-release" }, "devDependencies": { + "babel-plugin-transform-object-rest-spread": "6.26.0", "ban-sensitive-files": "1.9.2", "css-loader": "0.28.7", "cypress": "1.4.1", @@ -86,7 +87,6 @@ "semantic-action": "1.1.0", "simple-commit-message": "3.3.2", "standard": "10.0.3", - "vue": "2.5.13", "vue-loader": "13.6.1", "vue-template-compiler": "2.5.13", "vuex": "3.0.1" @@ -100,6 +100,7 @@ }, "dependencies": { "@cypress/webpack-preprocessor": "1.1.2", - "common-tags": "1.6.0" + "common-tags": "1.6.0", + "vue": "2.5.13" } } diff --git a/src/index.js b/src/index.js index 1483a37..93bc350 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,17 @@ +const Vue = require('vue/dist/vue') const { stripIndent } = require('common-tags') +// mountVue options +const defaultOptions = ['html', 'vue', 'base', 'mountId', 'extensions'] + +// default mount point element ID for root Vue instance +const defaultMountId = 'app' + +const parentDocument = window.parent.document +const projectName = Cypress.config('projectName') +const appIframeId = `Your App: '${projectName}'` +const appIframe = parentDocument.getElementById(appIframeId) + // having weak reference to styles prevents garbage collection // and "losing" styles when the next test starts const stylesCache = new Map() @@ -23,10 +35,6 @@ const copyStyles = component => { return } - const parentDocument = window.parent.document - const projectName = Cypress.config('projectName') - const appIframeId = `Your App: '${projectName}'` - const appIframe = parentDocument.getElementById(appIframeId) const head = appIframe.contentDocument.querySelector('head') styles.forEach(style => { head.appendChild(style) @@ -42,59 +50,20 @@ const deleteCachedConstructors = component => { Cypress._.values(component.components).forEach(deleteConstructor) } -const getVuePath = options => - options.vue || options.vuePath || '../node_modules/vue/dist/vue.js' - -const getVuexPath = options => options.vuex || options.vuexPath - const getPageHTML = options => { - if (options.html) { - return options.html - } - const vue = getVuePath(options) - let vuex = getVuexPath(options) - if (vuex) { - vuex = `` - } - - // note: add "base" tag to force loading static assets - // from the server, not from the "spec" file URL - if (options.base) { - if (vue.startsWith('.')) { - console.error( - 'You are using base tag %s and relative Vue path %s', - options.base, - vue - ) - console.error('the relative path might NOT work') - console.error( - 'maybe pass Vue url using "https://unpkg.com/vue/dist/vue.js"' - ) - } - return stripIndent` - - - - - -
- - - - ` - } - - const vueHtml = stripIndent` + return ( + options.html || + stripIndent` - + + ${options.base ? `` : ''} + -
- - ${vuex || ''} +
` - return vueHtml + ) } const registerGlobalComponents = (Vue, options) => { @@ -128,8 +97,7 @@ const installMixins = (Vue, options) => { } } -const isOptionName = name => - ['vue', 'html', 'vuePath', 'base', 'extensions'].includes(name) +const isOptionName = name => defaultOptions.includes(name) const isOptions = object => Object.keys(object).every(isOptionName) @@ -137,9 +105,8 @@ const isConstructor = object => object && object._compiled const hasStore = ({ store }) => store && store._vm -const forEachValue = (obj, fn) => { +const forEachValue = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key)) -} const resetStoreVM = (Vue, { store }) => { // bind store public getters @@ -177,38 +144,51 @@ const mountVue = (component, optionsOrProps = {}) => () => { props = optionsOrProps } - const vueHtml = getPageHTML(options) - const document = cy.state('document') - document.write(vueHtml) - document.close() - - // TODO: do not log out "its(Vue)" command - // but it currently does not support it - return cy - .window({ log: false }) - .its('Vue') - .then(Vue => { - // refresh inner Vue instance of Vuex store - if (hasStore(component)) { - component.store = resetStoreVM(Vue, component) - } - installMixins(Vue, options) - installPlugins(Vue, options) - registerGlobalComponents(Vue, options) - deleteCachedConstructors(component) - - if (isConstructor(component)) { - const Cmp = Vue.extend(component) - Cypress.vue = new Cmp(props).$mount('#app') - copyStyles(Cmp) - } else { - const allOptions = Object.assign({}, component, { el: '#app' }) - Cypress.vue = new Vue(allOptions) - copyStyles(component) - } - - return Cypress.vue - }) + // display deprecation warnings + if (options.vue) { + console.warn(stripIndent` + [DEPRECATION]: 'vue' option has been deprecated. + 'node_modules/vue/dis/vue' is always used. + Please remove it from your 'mountVue' options.`) + } + + // insert base app template + const doc = appIframe.contentDocument + doc.write(getPageHTML(options)) + doc.close() + + // get root Vue mount element + const mountId = options.mountId || defaultMountId + const el = doc.getElementById(mountId) + + // set global Vue instance: + // 1. convenience for debugging in DevTools + // 2. some libraries might check for this global + appIframe.contentWindow.Vue = Vue + + // refresh inner Vue instance of Vuex store + if (hasStore(component)) { + component.store = resetStoreVM(Vue, component) + } + + // setup Vue instance + 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(el) + copyStyles(Cmp) + } else { + Cypress.vue = new Vue(component).$mount(el) + copyStyles(component) + } + + return cy.wrap(Cypress.vue) } module.exports = mountVue