diff --git a/package.json b/package.json index 487c330aa..b4c34e494 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:fix": "npm run lint -- --fix", "release": "npm run build && npm run test:unit:only && lerna publish --conventional-commits -m \"chore(release): publish %s\" --cd-version prerelease", "test": "npm run lint && npm run lint:docs && npm run flow && npm run test:types && npm run test:unit && npm run test:unit:karma && npm run test:unit:node", - "test:compat": "scripts/test-compat.sh", + "test:compat": "scripts/test-compat-all.sh", "test:unit": "npm run build:test && npm run test:unit:only", "test:unit:only": "mocha-webpack --webpack-config test/setup/webpack.test.config.js test/specs --recursive --require test/setup/mocha.setup.js", "test:unit:debug": "npm run build:test && node --inspect-brk node_modules/.bin/mocha-webpack --webpack-config test/setup/webpack.test.config.js test/specs --recursive --require test/setup/mocha.setup.js", @@ -64,7 +64,7 @@ "sinon": "^2.3.2", "sinon-chai": "^2.10.0", "typescript": "^3.0.1", - "vee-validate": "2.1.0-beta.5", + "vee-validate": "^2.1.3", "vue": "2.5.16", "vue-class-component": "^6.1.2", "vue-loader": "^13.6.2", diff --git a/packages/create-instance/add-mocks.js b/packages/create-instance/add-mocks.js index 9b5a6cfee..ef83b5e14 100644 --- a/packages/create-instance/add-mocks.js +++ b/packages/create-instance/add-mocks.js @@ -3,8 +3,8 @@ import $$Vue from 'vue' import { warn } from 'shared/util' export default function addMocks ( - mockedProperties: Object | false = {}, - Vue: Component + _Vue: Component, + mockedProperties: Object | false = {} ): void { if (mockedProperties === false) { return @@ -12,7 +12,7 @@ export default function addMocks ( Object.keys(mockedProperties).forEach(key => { try { // $FlowIgnore - Vue.prototype[key] = mockedProperties[key] + _Vue.prototype[key] = mockedProperties[key] } catch (e) { warn( `could not overwrite property ${key}, this is ` + @@ -21,6 +21,6 @@ export default function addMocks ( ) } // $FlowIgnore - $$Vue.util.defineReactive(Vue, key, mockedProperties[key]) + $$Vue.util.defineReactive(_Vue, key, mockedProperties[key]) }) } diff --git a/packages/create-instance/add-stubs.js b/packages/create-instance/add-stubs.js index 591db4b89..e4049c1df 100644 --- a/packages/create-instance/add-stubs.js +++ b/packages/create-instance/add-stubs.js @@ -1,30 +1,8 @@ -import { - createStubsFromStubsObject, - createStubFromComponent -} from 'shared/create-component-stubs' import { addHook } from './add-hook' -export function addStubs (component, stubs, _Vue, shouldProxy) { - const stubComponents = createStubsFromStubsObject( - component.components, - stubs - ) - +export function addStubs (_Vue, stubComponents) { function addStubComponentsMixin () { - Object.assign( - this.$options.components, - stubComponents - ) - if (typeof Proxy !== 'undefined' && shouldProxy) { - this.$options.components = new Proxy(this.$options.components, { - set (target, prop, value) { - if (!target[prop]) { - target[prop] = createStubFromComponent(value, prop) - } - return true - } - }) - } + Object.assign(this.$options.components, stubComponents) } addHook(_Vue.options, 'beforeMount', addStubComponentsMixin) diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index 7b2a8f761..44d99f4d9 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -9,13 +9,13 @@ import { compileTemplate, compileTemplateForSlots } from 'shared/compile-template' -import { isRequiredComponent } from 'shared/validators' import extractInstanceOptions from './extract-instance-options' import createFunctionalComponent from './create-functional-component' import { componentNeedsCompiling, isPlainObject } from 'shared/validators' import { validateSlots } from './validate-slots' import createScopedSlots from './create-scoped-slots' -import { extendExtendedComponents } from './extend-extended-components' +import { createStubsFromStubsObject } from 'shared/create-component-stubs' +import { patchRender } from './patch-render' function vueExtendUnsupportedOption (option: string) { return `options.${option} is not supported for ` + @@ -56,10 +56,16 @@ export default function createInstance ( // instance options are options that are passed to the // root instance when it's instantiated const instanceOptions = extractInstanceOptions(options) + const stubComponentsObject = createStubsFromStubsObject( + component.components, + // $FlowIgnore + options.stubs + ) addEventLogger(_Vue) - addMocks(options.mocks, _Vue) - addStubs(component, options.stubs, _Vue, options.shouldProxy) + addMocks(_Vue, options.mocks) + addStubs(_Vue, stubComponentsObject) + patchRender(_Vue, stubComponentsObject, options.shouldProxy) if ( (component.options && component.options.functional) || @@ -77,29 +83,6 @@ export default function createInstance ( compileTemplate(component) } - // Replace globally registered components with components extended - // from localVue. - // Vue version must be 2.3 or greater, because of a bug resolving - // extended constructor options (https://github.com/vuejs/vue/issues/4976) - if (vueVersion > 2.2) { - for (const c in _Vue.options.components) { - if (!isRequiredComponent(c)) { - const comp = _Vue.options.components[c] - const options = comp.options ? comp.options : comp - const extendedComponent = _Vue.extend(options) - extendedComponent.options.$_vueTestUtils_original = comp - _Vue.component(c, extendedComponent) - } - } - } - - extendExtendedComponents( - component, - _Vue, - options.logModifiedComponents, - instanceOptions.components - ) - if (component.options) { component.options._base = _Vue } @@ -112,6 +95,7 @@ export default function createInstance ( // used to identify extended component using constructor Constructor.options.$_vueTestUtils_original = component + if (options.slots) { compileTemplateForSlots(options.slots) // validate slots outside of the createSlots function so @@ -143,6 +127,8 @@ export default function createInstance ( const parentComponentOptions = options.parentComponent || {} parentComponentOptions.provide = options.provide + parentComponentOptions.$_doNotStubChildren = true + parentComponentOptions.render = function (h) { const slots = options.slots ? createSlotVNodes(this, options.slots) diff --git a/packages/create-instance/extend-extended-components.js b/packages/create-instance/extend-extended-components.js deleted file mode 100644 index a086b18ba..000000000 --- a/packages/create-instance/extend-extended-components.js +++ /dev/null @@ -1,95 +0,0 @@ -import { warn } from 'shared/util' -import { addHook } from './add-hook' - -function resolveComponents (options = {}, components = {}) { - let extendOptions = options.extendOptions - while (extendOptions) { - resolveComponents(extendOptions, components) - extendOptions = extendOptions.extendOptions - } - let extendsFrom = options.extends - while (extendsFrom) { - resolveComponents(extendsFrom, components) - extendsFrom = extendsFrom.extends - } - Object.keys(options.components || {}).forEach((c) => { - components[c] = options.components[c] - }) - return components -} - -function shouldExtend (component) { - while (component) { - if (component.extendOptions) { - return true - } - component = component.extends - } -} - -// Components created with Vue.extend are not created internally in Vue -// by extending a localVue constructor. To make sure they inherit -// properties add to a localVue constructor, we must create new components by -// extending the original extended components from the localVue constructor. -// We apply a global mixin that overwrites the components original -// components with the extended components when they are created. -export function extendExtendedComponents ( - component, - _Vue, - logModifiedComponents, - excludedComponents = { }, - stubAllComponents = false -) { - const extendedComponents = Object.create(null) - const components = resolveComponents(component) - - Object.keys(components).forEach(c => { - const comp = components[c] - const shouldExtendComponent = - (shouldExtend(comp) && - !excludedComponents[c]) || - stubAllComponents - if (shouldExtendComponent) { - if (logModifiedComponents) { - warn( - `The child component <${c}> has been modified to ensure ` + - `it is created with properties injected by Vue Test Utils. \n` + - `This is because the component was created with Vue.extend, ` + - `or uses the Vue Class Component decorator. \n` + - `Because the component has been modified, it is not possible ` + - `to find it with a component selector. To find the ` + - `component, you must stub it manually using the stubs mounting ` + - `option, or use a name or ref selector. \n` + - `You can hide this warning by setting the Vue Test Utils ` + - `config.logModifiedComponents option to false.` - ) - } - const extendedComp = _Vue.extend(comp.options) - // Used to identify component in a render tree - extendedComp.options.$_vueTestUtils_original = comp - extendedComponents[c] = extendedComp - } - // If a component has been replaced with an extended component - // all its child components must also be replaced. - extendExtendedComponents( - comp, - _Vue, - logModifiedComponents, - {}, - shouldExtendComponent - ) - }) - if (Object.keys(extendedComponents).length > 0) { - addHook(_Vue.options, 'beforeCreate', function addExtendedOverwrites () { - if ( - this.constructor.extendOptions === component || - this.$options.$_vueTestUtils_original === component - ) { - Object.assign( - this.$options.components, - extendedComponents - ) - } - }) - } -} diff --git a/packages/create-instance/patch-render.js b/packages/create-instance/patch-render.js new file mode 100644 index 000000000..b30b774bf --- /dev/null +++ b/packages/create-instance/patch-render.js @@ -0,0 +1,121 @@ +import { createStubFromComponent } from 'shared/create-component-stubs' +import { resolveComponent, semVerGreaterThan } from 'shared/util' +import { isReservedTag } from 'shared/validators' +import { addHook } from './add-hook' +import Vue from 'vue' + +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' +const LIFECYCLE_HOOK = semVerGreaterThan(Vue.version, '2.1.8') + ? 'beforeCreate' + : 'beforeMount' + +function shouldExtend (component, _Vue) { + return ( + (typeof component === 'function' && !isDynamicComponent(component)) || + (component && component.extends) + ) +} + +function extend (component, _Vue) { + const stub = _Vue.extend(component.options) + stub.options.$_vueTestUtils_original = component + return stub +} + +function createStubIfNeeded (shouldStub, component, _Vue, el) { + if (shouldStub) { + return createStubFromComponent(component || {}, el) + } + + if (shouldExtend(component, _Vue)) { + return extend(component, _Vue) + } +} + +function shouldNotBeStubbed (el, whitelist, modifiedComponents) { + return ( + (typeof el === 'string' && isReservedTag(el)) || + isWhitelisted(el, whitelist) || + isAlreadyStubbed(el, modifiedComponents) + ) +} + +function isConstructor (el) { + return typeof el === 'function' +} + +export function patchRender (_Vue, stubs, stubAllComponents) { + // This mixin patches vm.$createElement so that we can stub all components + // before they are rendered in shallow mode. We also need to ensure that + // component constructors were created from the _Vue constructor. If not, + // we must replace them with components created from the _Vue constructor + // before calling the original $createElement. This ensures that components + // have the correct instance properties and stubs when they are rendered. + function patchRenderMixin () { + const vm = this + + if (vm.$options.$_doNotStubChildren || vm._isFunctionalContainer) { + return + } + + const modifiedComponents = new Set() + const originalCreateElement = vm.$createElement + const originalComponents = vm.$options.components + + const createElement = (el, ...args) => { + if (shouldNotBeStubbed(el, stubs, modifiedComponents)) { + return originalCreateElement(el, ...args) + } + + if (isConstructor(el)) { + if (stubAllComponents) { + const stub = createStubFromComponent(el, el.name || 'anonymous') + return originalCreateElement(stub, ...args) + } + + const Constructor = shouldExtend(el, _Vue) ? extend(el, _Vue) : el + + return originalCreateElement(Constructor, ...args) + } + + if (typeof el === 'string') { + let original = resolveComponent(el, originalComponents) + + if ( + original && + original.options && + original.options.$_vueTestUtils_original + ) { + original = original.options.$_vueTestUtils_original + } + + if (isDynamicComponent(original)) { + return originalCreateElement(el, ...args) + } + + const stub = createStubIfNeeded(stubAllComponents, original, _Vue, el) + + if (stub) { + vm.$options.components = { + ...vm.$options.components, + [el]: stub + } + modifiedComponents.add(el) + } + } + + return originalCreateElement(el, ...args) + } + + vm[CREATE_ELEMENT_ALIAS] = createElement + vm.$createElement = createElement + } + + addHook(_Vue.options, LIFECYCLE_HOOK, patchRenderMixin) +} diff --git a/packages/shared/create-component-stubs.js b/packages/shared/create-component-stubs.js index 3396d2b22..82e034610 100644 --- a/packages/shared/create-component-stubs.js +++ b/packages/shared/create-component-stubs.js @@ -81,6 +81,7 @@ export function createStubFromComponent ( return { ...getCoreProperties(componentOptions), $_vueTestUtils_original: originalComponent, + $_doNotStubChildren: true, render (h, context) { return h( tagName, @@ -117,6 +118,7 @@ export function createStubFromString ( return { ...getCoreProperties(componentOptions), + $_doNotStubChildren: true, ...compileFromString(templateString) } } @@ -172,60 +174,3 @@ export function createStubsFromStubsObject ( return acc }, {}) } - -function stubComponents ( - components: Components, - stubbedComponents: Components -): void { - for (const component in components) { - const cmp = components[component] - const componentOptions = typeof cmp === 'function' - ? cmp.extendOptions - : cmp - - if (!componentOptions) { - stubbedComponents[component] = createStubFromComponent( - {}, - component - ) - return - } - - stubbedComponents[component] = createStubFromComponent( - cmp, - component - ) - } -} - -export function createStubsForComponent ( - component: Component -): Components { - const stubbedComponents = {} - - if (component.options) { - stubComponents(component.options.components, stubbedComponents) - } - - if (component.components) { - stubComponents(component.components, stubbedComponents) - } - - let extended = component.extends - while (extended) { - if (extended.components) { - stubComponents(extended.components, stubbedComponents) - } - extended = extended.extends - } - - let extendOptions = component.extendOptions - while (extendOptions) { - if (extendOptions && extendOptions.components) { - stubComponents(extendOptions.components, stubbedComponents) - } - extendOptions = extendOptions.extendOptions - } - - return stubbedComponents -} diff --git a/packages/shared/util.js b/packages/shared/util.js index 1b78c8506..e159385b7 100644 --- a/packages/shared/util.js +++ b/packages/shared/util.js @@ -10,10 +10,10 @@ export function warn (msg: string): void { } const camelizeRE = /-(\w)/g + export const camelize = (str: string): string => { - const camelizedStr = str.replace( - camelizeRE, - (_, c) => (c ? c.toUpperCase() : '') + const camelizedStr = str.replace(camelizeRE, (_, c) => + c ? c.toUpperCase() : '' ) return camelizedStr.charAt(0).toLowerCase() + camelizedStr.slice(1) } @@ -34,3 +34,41 @@ export const hyphenate = (str: string): string => export const vueVersion = Number( `${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}` ) + +function hasOwnProperty (obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop) +} + +export function resolveComponent (id: string, components: Object) { + if (typeof id !== 'string') { + return + } + // check local registration variations first + if (hasOwnProperty(components, id)) { + return components[id] + } + var camelizedId = camelize(id) + if (hasOwnProperty(components, camelizedId)) { + return components[camelizedId] + } + var PascalCaseId = capitalize(camelizedId) + if (hasOwnProperty(components, PascalCaseId)) { + return components[PascalCaseId] + } + // fallback to prototype chain + 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 + } + return false +} diff --git a/packages/shared/validators.js b/packages/shared/validators.js index 8259e753f..c24453a2d 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -95,3 +95,42 @@ export function isRequiredComponent (name: string): boolean { name === 'KeepAlive' || name === 'Transition' || name === 'TransitionGroup' ) } + +function makeMap ( + str: string, + expectsLowerCase?: boolean +) { + var map = Object.create(null) + var list = str.split(',') + for (var i = 0; i < list.length; i++) { + map[list[i]] = true + } + return expectsLowerCase + ? function (val: string) { return map[val.toLowerCase()] } + : function (val: string) { return map[val] } +} + +export const isHTMLTag = makeMap( + 'html,body,base,head,link,meta,style,title,' + + 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + + 'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' + + 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' + + 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,' + + 'embed,object,param,source,canvas,script,noscript,del,ins,' + + 'caption,col,colgroup,table,thead,tbody,td,th,tr,video,' + + 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' + + 'output,progress,select,textarea,' + + 'details,dialog,menu,menuitem,summary,' + + 'content,element,shadow,template,blockquote,iframe,tfoot' +) + +// this map is intentionally selective, only covering SVG elements that may +// contain child elements. +export const isSVG = makeMap( + 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' + + 'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' + + 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view', + true +) + +export const isReservedTag = (tag: string) => isHTMLTag(tag) || isSVG(tag) diff --git a/packages/test-utils/src/shallow-mount.js b/packages/test-utils/src/shallow-mount.js index dd77dbd2b..2376c4495 100644 --- a/packages/test-utils/src/shallow-mount.js +++ b/packages/test-utils/src/shallow-mount.js @@ -1,40 +1,14 @@ // @flow -import './warn-if-no-window' -import Vue from 'vue' import mount from './mount' import type VueWrapper from './vue-wrapper' -import { - createStubsForComponent, - createStubFromComponent -} from '../../shared/create-component-stubs' -import { normalizeStubs } from '../../shared/normalize' export default function shallowMount ( component: Component, options: Options = {} ): VueWrapper { - const _Vue = options.localVue || Vue - - options.stubs = normalizeStubs(options.stubs) - - // Vue registers a recursive component on the original options - // This stub will override the component added by Vue - // $FlowIgnore - if (options.stubs && !options.stubs[component.name]) { - // $FlowIgnore - options.stubs[component.name] = createStubFromComponent( - component, - component.name - ) - } - return mount(component, { ...options, - shouldProxy: true, - components: { - ...createStubsForComponent(_Vue), - ...createStubsForComponent(component) - } + shouldProxy: true }) } diff --git a/test/specs/external-libraries.spec.js b/test/specs/external-libraries.spec.js index 9027d454e..06b2a4537 100644 --- a/test/specs/external-libraries.spec.js +++ b/test/specs/external-libraries.spec.js @@ -3,7 +3,7 @@ import VeeValidate from 'vee-validate' import { describeWithShallowAndMount } from '~resources/utils' describeWithShallowAndMount('external libraries', () => { - it.skip('works with vee validate', () => { + it('works with vee validate', () => { const TestComponent = { template: '
' } diff --git a/test/specs/mount.spec.js b/test/specs/mount.spec.js index 1a7138529..cdbdfcef4 100644 --- a/test/specs/mount.spec.js +++ b/test/specs/mount.spec.js @@ -27,7 +27,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { expect(wrapper.vm).to.be.an('object') }) - it('returns new VueWrapper with mounted Vue instance when root is functional component', () => { + it('returns new VueWrapper when root is functional component', () => { const FunctionalComponent = { functional: true, render (h) { @@ -47,7 +47,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { expect(wrapper.findAll('p').length).to.equal(2) }) - it('returns new VueWrapper with mounted Vue instance with props, if passed as propsData', () => { + it('returns new VueWrapper with correct props data', () => { const prop1 = { test: 'TEST' } const wrapper = mount(ComponentWithProps, { propsData: { prop1 }}) expect(wrapper.vm).to.be.an('object') @@ -229,31 +229,6 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { }) }) - it('logs if component is extended', () => { - const msg = - `[vue-test-utils]: The child componentHello
World
-