Skip to content

Commit

Permalink
fix: refactor Vue initialization in app frame
Browse files Browse the repository at this point in the history
Refactors how the Vue instance within the component app frame is initialized, and fully fixes Vue integration.
- fixes lingering issues after pull request cypress-io#13
- fully fixes cypress-io#6
- fixes #1
- README docs have been updated to reflect refactoring
- 'vue' option has been deprecated (with warning to user) as it's no longer necessary
- spread operator support has been added for upcoming Vuex example
  • Loading branch information
amirrustam committed Jan 28, 2018
1 parent 45e5276 commit e877338
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 95 deletions.
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
```
Expand All @@ -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: `<div id="app"></div><script src="${vue}"></script>`
html: `<div id="app"></div><script src="${polyfill}"></script>`
}
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

Expand Down
5 changes: 5 additions & 0 deletions components/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-object-rest-spread"
]
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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"
}
}
154 changes: 67 additions & 87 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)
Expand All @@ -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 = `<script src="${vuex}"></script>`
}

// 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`
<html>
<head>
<base href="${options.base}" />
</head>
<body>
<div id="app"></div>
<script src="${vue}"></script>
</body>
</html>
`
}

const vueHtml = stripIndent`
return (
options.html ||
stripIndent`
<html>
<head></head>
<head>
${options.base ? `<base href="${options.base}" />` : ''}
</head>
<body>
<div id="app"></div>
<script src="${vue}"></script>
${vuex || ''}
<div id="${options.mountId || defaultMountId}"></div>
</body>
</html>
`
return vueHtml
)
}

const registerGlobalComponents = (Vue, options) => {
Expand Down Expand Up @@ -128,18 +97,16 @@ 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)

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
Expand Down Expand Up @@ -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

0 comments on commit e877338

Please sign in to comment.