diff --git a/flow/modules.flow.js b/flow/modules.flow.js index c8b07f4d3..326da0fa7 100644 --- a/flow/modules.flow.js +++ b/flow/modules.flow.js @@ -23,3 +23,7 @@ declare module 'vue-server-renderer' { declare module 'cheerio' { declare module.exports: any; } + +declare module 'semver' { + declare module.exports: any; +} diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index d895af758..182f36097 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -4,7 +4,8 @@ import { createSlotVNodes } from './create-slot-vnodes' import addMocks from './add-mocks' import { addEventLogger } from './log-events' import { addStubs } from './add-stubs' -import { throwError, vueVersion } from 'shared/util' +import { throwError } from 'shared/util' +import { VUE_VERSION } from 'shared/consts' import { compileTemplate, compileTemplateForSlots @@ -42,7 +43,7 @@ export default function createInstance ( _Vue.options._base = _Vue if ( - vueVersion < 2.3 && + VUE_VERSION < 2.3 && typeof component === 'function' && component.options ) { @@ -110,7 +111,7 @@ export default function createInstance ( if ( options.provide && typeof options.provide === 'object' && - vueVersion < 2.5 + VUE_VERSION < 2.5 ) { const obj = { ...options.provide } options.provide = () => obj diff --git a/packages/create-instance/create-scoped-slots.js b/packages/create-instance/create-scoped-slots.js index b1ee39a17..47a9829f7 100644 --- a/packages/create-instance/create-scoped-slots.js +++ b/packages/create-instance/create-scoped-slots.js @@ -1,7 +1,8 @@ // @flow import { compileToFunctions } from 'vue-template-compiler' -import { throwError, vueVersion } from 'shared/util' +import { throwError } from 'shared/util' +import { VUE_VERSION } from 'shared/consts' function isDestructuringSlotScope (slotScope: string): boolean { return slotScope[0] === '{' && slotScope[slotScope.length - 1] === '}' @@ -39,7 +40,7 @@ function getVueTemplateCompilerHelpers ( } function validateEnvironment (): void { - if (vueVersion < 2.1) { + if (VUE_VERSION < 2.1) { throwError(`the scopedSlots option is only supported in vue@2.1+.`) } } diff --git a/packages/create-instance/patch-render.js b/packages/create-instance/patch-render.js index 43387d6a7..598f434e2 100644 --- a/packages/create-instance/patch-render.js +++ b/packages/create-instance/patch-render.js @@ -1,17 +1,15 @@ import { createStubFromComponent } from './create-component-stubs' -import { resolveComponent, semVerGreaterThan } from 'shared/util' +import { resolveComponent } from 'shared/util' import { isReservedTag } from 'shared/validators' -import Vue from 'vue' -import { BEFORE_RENDER_LIFECYCLE_HOOK } from 'shared/consts' +import { + BEFORE_RENDER_LIFECYCLE_HOOK, + CREATE_ELEMENT_ALIAS +} from 'shared/consts' const isWhitelisted = (el, whitelist) => resolveComponent(el, whitelist) const isAlreadyStubbed = (el, stubs) => stubs.has(el) const isDynamicComponent = cmp => typeof cmp === 'function' && !cmp.cid -const CREATE_ELEMENT_ALIAS = semVerGreaterThan(Vue.version, '2.1.5') - ? '_c' - : '_h' - function shouldExtend (component, _Vue) { return ( (typeof component === 'function' && !isDynamicComponent(component)) || diff --git a/packages/shared/consts.js b/packages/shared/consts.js index 30b437426..5e74370b9 100644 --- a/packages/shared/consts.js +++ b/packages/shared/consts.js @@ -1,18 +1,24 @@ import Vue from 'vue' -import { semVerGreaterThan } from './util' +import semver from 'semver' export const NAME_SELECTOR = 'NAME_SELECTOR' export const COMPONENT_SELECTOR = 'COMPONENT_SELECTOR' export const REF_SELECTOR = 'REF_SELECTOR' export const DOM_SELECTOR = 'DOM_SELECTOR' export const INVALID_SELECTOR = 'INVALID_SELECTOR' + export const VUE_VERSION = Number( `${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}` ) + export const FUNCTIONAL_OPTIONS = VUE_VERSION >= 2.5 ? 'fnOptions' : 'functionalOptions' export const BEFORE_RENDER_LIFECYCLE_HOOK = - semVerGreaterThan(Vue.version, '2.1.8') + semver.gt(Vue.version, '2.1.8') ? 'beforeCreate' : 'beforeMount' + +export const CREATE_ELEMENT_ALIAS = semver.gt(Vue.version, '2.1.5') + ? '_c' + : '_h' diff --git a/packages/shared/package.json b/packages/shared/package.json index e8e37b938..00f919439 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -2,6 +2,9 @@ "name": "shared", "version": "1.0.0-beta.27", "private": true, + "dependencies": { + "semver": "^5.6.0" + }, "peerDependencies": { "vue": "2.x", "vue-template-compiler": "^2.x" diff --git a/packages/shared/util.js b/packages/shared/util.js index e159385b7..3a7198c33 100644 --- a/packages/shared/util.js +++ b/packages/shared/util.js @@ -1,5 +1,6 @@ // @flow import Vue from 'vue' +import semver from 'semver' export function throwError (msg: string): void { throw new Error(`[vue-test-utils]: ${msg}`) @@ -31,10 +32,6 @@ const hyphenateRE = /\B([A-Z])/g export const hyphenate = (str: string): string => str.replace(hyphenateRE, '-$1').toLowerCase() -export const vueVersion = Number( - `${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}` -) - function hasOwnProperty (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop) } @@ -59,16 +56,28 @@ export function resolveComponent (id: string, components: Object) { return components[id] || components[camelizedId] || components[PascalCaseId] } -export function semVerGreaterThan (a: string, b: string) { - const pa = a.split('.') - const pb = b.split('.') - for (let i = 0; i < 3; i++) { - var na = Number(pa[i]) - var nb = Number(pb[i]) - if (na > nb) return true - if (nb > na) return false - if (!isNaN(na) && isNaN(nb)) return true - if (isNaN(na) && !isNaN(nb)) return false +const UA = typeof window !== 'undefined' && + 'navigator' in window && + navigator.userAgent.toLowerCase() + +export const isPhantomJS = UA && UA.includes && + UA.match(/phantomjs/i) + +export const isEdge = UA && UA.indexOf('edge/') > 0 +export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge + +// get the event used to trigger v-model handler that updates bound data +export function getCheckedEvent () { + const version = Vue.version + + if (semver.satisfies(version, '2.1.9 - 2.1.10')) { + return 'click' + } + + if (semver.satisfies(version, '2.2 - 2.4')) { + return isChrome ? 'click' : 'change' } - return false + + // change is handler for version 2.0 - 2.1.8, and 2.5+ + return 'change' } diff --git a/packages/test-utils/src/find.js b/packages/test-utils/src/find.js index c4e654be0..54dc213fe 100644 --- a/packages/test-utils/src/find.js +++ b/packages/test-utils/src/find.js @@ -4,9 +4,10 @@ import findDOMNodes from './find-dom-nodes' import { DOM_SELECTOR, REF_SELECTOR, - COMPONENT_SELECTOR + COMPONENT_SELECTOR, + VUE_VERSION } from 'shared/consts' -import { throwError, vueVersion } from 'shared/util' +import { throwError } from 'shared/util' import { matches } from './matches' export function findAllInstances (rootVm: any) { @@ -74,7 +75,7 @@ export default function find ( (selector.value.options && selector.value.options.functional) ) && - vueVersion < 2.3 + VUE_VERSION < 2.3 ) { throwError( `find for functional components is not supported ` + diff --git a/packages/test-utils/src/wrapper.js b/packages/test-utils/src/wrapper.js index 4d13e0c25..36fd4128a 100644 --- a/packages/test-utils/src/wrapper.js +++ b/packages/test-utils/src/wrapper.js @@ -4,12 +4,18 @@ import Vue from 'vue' import getSelector from './get-selector' import { REF_SELECTOR, - FUNCTIONAL_OPTIONS + FUNCTIONAL_OPTIONS, + VUE_VERSION } from 'shared/consts' import config from './config' import WrapperArray from './wrapper-array' import ErrorWrapper from './error-wrapper' -import { throwError, warn, vueVersion } from 'shared/util' +import { + throwError, + warn, + getCheckedEvent, + isPhantomJS +} from 'shared/util' import find from './find' import createWrapper from './create-wrapper' import { orderWatchers } from './order-watchers' @@ -40,7 +46,7 @@ export default class Wrapper implements BaseWrapper { // $FlowIgnore : issue with defineProperty Object.defineProperty(this, 'rootNode', { get: () => vnode || element, - set: () => throwError('wrapper.vnode is read-only') + set: () => throwError('wrapper.rootNode is read-only') }) // $FlowIgnore Object.defineProperty(this, 'vnode', { @@ -487,45 +493,38 @@ export default class Wrapper implements BaseWrapper { const tagName = this.element.tagName // $FlowIgnore const type = this.attributes().type + const event = getCheckedEvent() - if (tagName === 'SELECT') { - throwError( - `wrapper.setChecked() cannot be called on a ` + - ` ` + - `element.` + `parameter false on a ` + + `element.` ) - } else { + } + + if (event !== 'click' || isPhantomJS) { // $FlowIgnore - if (!this.element.checked) { - this.trigger('click') - this.trigger('change') - } + this.element.selected = true } - } else if (tagName === 'INPUT' || tagName === 'TEXTAREA') { - throwError( - `wrapper.setChecked() cannot be called on "text" ` + - `inputs. Use wrapper.setValue() instead` - ) - } else { - throwError(`wrapper.setChecked() cannot be called on this element`) + this.trigger(event) + return } + + throwError(`wrapper.setChecked() cannot be called on this element`) } /** @@ -533,47 +532,32 @@ export default class Wrapper implements BaseWrapper { */ setSelected (): void { const tagName = this.element.tagName - // $FlowIgnore - const type = this.attributes().type + + if (tagName === 'SELECT') { + throwError( + `wrapper.setSelected() cannot be called on select. ` + + `Call it on one of its options` + ) + } if (tagName === 'OPTION') { // $FlowIgnore this.element.selected = true // $FlowIgnore - if (this.element.parentElement.tagName === 'OPTGROUP') { - // $FlowIgnore - createWrapper(this.element.parentElement.parentElement, this.options) - .trigger('change') - } else { + let parentElement = this.element.parentElement + + // $FlowIgnore + if (parentElement.tagName === 'OPTGROUP') { // $FlowIgnore - createWrapper(this.element.parentElement, this.options) - .trigger('change') + parentElement = parentElement.parentElement } - } else if (tagName === 'SELECT') { - throwError( - `wrapper.setSelected() cannot be called on select. ` + - `Call it on one of its options` - ) - } else if (tagName === 'INPUT' && type === 'checkbox') { - throwError( - `wrapper.setSelected() cannot be called on a element. Use ` + - `wrapper.setChecked() instead` - ) - } else if (tagName === 'INPUT' && type === 'radio') { - throwError( - `wrapper.setSelected() cannot be called on a element. Use wrapper.setChecked() ` + - `instead` - ) - } else if (tagName === 'INPUT' || tagName === 'TEXTAREA') { - throwError( - `wrapper.setSelected() cannot be called on "text" ` + - `inputs. Use wrapper.setValue() instead` - ) - } else { - throwError(`wrapper.setSelected() cannot be called on this element`) + + // $FlowIgnore + createWrapper(parentElement, this.options).trigger('change') + return } + + throwError(`wrapper.setSelected() cannot be called on this element`) } /** @@ -595,7 +579,7 @@ export default class Wrapper implements BaseWrapper { ) Object.keys(computed).forEach(key => { - if (vueVersion > 2.1) { + if (VUE_VERSION > 2.1) { // $FlowIgnore : Problem with possibly null this.vm if (!this.vm._computedWatchers[key]) { throwError( @@ -731,7 +715,7 @@ export default class Wrapper implements BaseWrapper { !this.vm.$options._propKeys || !this.vm.$options._propKeys.some(prop => prop === key) ) { - if (vueVersion > 2.3) { + if (VUE_VERSION > 2.3) { // $FlowIgnore : Problem with possibly null this.vm this.vm.$attrs[key] = data[key] return @@ -771,11 +755,7 @@ export default class Wrapper implements BaseWrapper { // $FlowIgnore const type = this.attributes().type - if (tagName === 'SELECT') { - // $FlowIgnore - this.element.value = value - this.trigger('change') - } else if (tagName === 'OPTION') { + if (tagName === 'OPTION') { throwError( `wrapper.setValue() cannot be called on an