From 0d1ddd1e52bc66ed4fb7a1388be612f874dc9c57 Mon Sep 17 00:00:00 2001 From: Edd Yerburgh Date: Sun, 26 Aug 2018 17:33:47 +0100 Subject: [PATCH] fix: handle global stubs and functional extended components (#943) --- package.json | 3 +- packages/create-instance/add-hook.js | 10 +++++++ packages/create-instance/add-stubs.js | 11 ++++---- packages/create-instance/create-instance.js | 17 ++++++----- .../extend-extended-components.js | 25 ++++++++--------- packages/create-instance/log-events.js | 16 +++++------ packages/server-test-utils/package.json | 2 +- .../server-test-utils/src/renderToString.js | 4 ++- packages/test-utils/src/matches.js | 8 ++++-- packages/test-utils/src/mount.js | 5 ++-- test/specs/create-local-vue.spec.js | 6 ---- test/specs/external-libraries.spec.js | 17 +++++++++++ test/specs/mount.spec.js | 28 ++++++++++++++++++- test/specs/wrapper/find.spec.js | 16 +++++++++++ yarn.lock | 15 ++++++---- 15 files changed, 127 insertions(+), 56 deletions(-) create mode 100644 packages/create-instance/add-hook.js create mode 100644 test/specs/external-libraries.spec.js diff --git a/package.json b/package.json index 32ee534e7..8faed4e21 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,8 @@ "rollup": "^0.58.2", "sinon": "^2.3.2", "sinon-chai": "^2.10.0", + "typescript": "^3.0.1", + "vee-validate": "2.1.0-beta.5", "vue": "2.5.16", "vue-class-component": "^6.1.2", "vue-loader": "^13.6.2", @@ -71,7 +73,6 @@ "vue-template-compiler": "2.5.16", "vuepress": "^0.14.2", "vuepress-theme-vue": "^1.0.3", - "vuetify": "^0.16.9", "vuex": "^3.0.1", "webpack": "^3.0.1", "webpack-node-externals": "^1.6.0" diff --git a/packages/create-instance/add-hook.js b/packages/create-instance/add-hook.js new file mode 100644 index 000000000..3647d27b8 --- /dev/null +++ b/packages/create-instance/add-hook.js @@ -0,0 +1,10 @@ +// This is used instead of Vue.mixin. The reason is that +// Vue.mixin is slower, and remove modfied options +// https://github.com/vuejs/vue/issues/8710 + +export function addHook (options, hook, fn) { + if (options[hook] && !Array.isArray(options[hook])) { + options[hook] = [options[hook]] + } + (options[hook] || (options[hook] = [])).push(fn) +} diff --git a/packages/create-instance/add-stubs.js b/packages/create-instance/add-stubs.js index e34a6a8cf..dbf5ded21 100644 --- a/packages/create-instance/add-stubs.js +++ b/packages/create-instance/add-stubs.js @@ -1,4 +1,5 @@ import { createStubsFromStubsObject } from 'shared/create-component-stubs' +import { addHook } from './add-hook' export function addStubs (component, stubs, _Vue) { const stubComponents = createStubsFromStubsObject( @@ -13,10 +14,8 @@ export function addStubs (component, stubs, _Vue) { ) } - _Vue.mixin({ - beforeMount: addStubComponentsMixin, - // beforeCreate is for components created in node, which - // never mount - beforeCreate: addStubComponentsMixin - }) + addHook(_Vue.options, 'beforeMount', addStubComponentsMixin) + // beforeCreate is for components created in node, which + // never mount + addHook(_Vue.options, 'beforeCreate', addStubComponentsMixin) } diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index c286f0dab..b6a7436b4 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -16,7 +16,6 @@ import { componentNeedsCompiling, isPlainObject } from 'shared/validators' import { validateSlots } from './validate-slots' import createScopedSlots from './create-scoped-slots' import { extendExtendedComponents } from './extend-extended-components' -import Vue from 'vue' function vueExtendUnsupportedOption (option: string) { return `options.${option} is not supported for ` + @@ -36,15 +35,12 @@ const UNSUPPORTED_VERSION_OPTIONS = [ export default function createInstance ( component: Component, - options: Options + options: Options, + _Vue: Component ): Component { // Remove cached constructor delete component._Ctor - const _Vue = options.localVue - ? options.localVue.extend() - : Vue.extend() - // make sure all extends are based on this instance _Vue.options._base = _Vue @@ -75,7 +71,8 @@ export default function createInstance ( component = createFunctionalComponent(component, options) } else if (options.context) { throwError( - `mount.context can only be used when mounting a ` + `functional component` + `mount.context can only be used when mounting a ` + + `functional component` ) } @@ -84,13 +81,15 @@ export default function createInstance ( } // Replace globally registered components with components extended - // from localVue. This makes sure the beforeMount mixins to add stubs - // is applied to globally registered components. + // 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 extendedComponent = _Vue.extend(_Vue.options.components[c]) + extendedComponent.options.$_vueTestUtils_original = + _Vue.options.components[c] _Vue.component(c, _Vue.extend(_Vue.options.components[c])) } } diff --git a/packages/create-instance/extend-extended-components.js b/packages/create-instance/extend-extended-components.js index ff8935a64..20d2da58d 100644 --- a/packages/create-instance/extend-extended-components.js +++ b/packages/create-instance/extend-extended-components.js @@ -1,4 +1,5 @@ import { warn } from 'shared/util' +import { addHook } from './add-hook' function createdFrom (extendOptions, componentOptions) { while (extendOptions) { @@ -75,12 +76,10 @@ export function extendExtendedComponents ( `config.logModifiedComponents option to false.` ) } + const extendedComp = _Vue.extend(comp) - // used to identify instance when calling find with component selector - if (extendedComp.extendOptions.options) { - extendedComp.extendOptions.options.$_vueTestUtils_original = comp - } - extendedComp.extendOptions.$_vueTestUtils_original = comp + // 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 @@ -93,15 +92,13 @@ export function extendExtendedComponents ( shouldExtendComponent ) }) - if (extendedComponents) { - _Vue.mixin({ - created () { - if (createdFrom(this.constructor, component)) { - Object.assign( - this.$options.components, - extendedComponents - ) - } + if (Object.keys(extendedComponents).length > 0) { + addHook(_Vue.options, 'beforeCreate', function addExtendedOverwrites () { + if (createdFrom(this.constructor, component)) { + Object.assign( + this.$options.components, + extendedComponents + ) } }) } diff --git a/packages/create-instance/log-events.js b/packages/create-instance/log-events.js index 2e71e4bcc..dfc964357 100644 --- a/packages/create-instance/log-events.js +++ b/packages/create-instance/log-events.js @@ -1,4 +1,5 @@ // @flow +import { addHook } from './add-hook' export function logEvents ( vm: Component, @@ -13,12 +14,11 @@ export function logEvents ( } } -export function addEventLogger (vue: Component): void { - vue.mixin({ - beforeCreate: function () { - this.__emitted = Object.create(null) - this.__emittedByOrder = [] - logEvents(this, this.__emitted, this.__emittedByOrder) - } - }) +export function addEventLogger (_Vue: Component): void { + addHook(_Vue.options, 'beforeCreate', function () { + this.__emitted = Object.create(null) + this.__emittedByOrder = [] + logEvents(this, this.__emitted, this.__emittedByOrder) + } + ) } diff --git a/packages/server-test-utils/package.json b/packages/server-test-utils/package.json index 5d3d2cd44..8be71635b 100644 --- a/packages/server-test-utils/package.json +++ b/packages/server-test-utils/package.json @@ -37,7 +37,7 @@ "typescript": "^2.6.2" }, "peerDependencies": { - "@vue/test-utils": "1.x", + "@vue/test-utils": "*", "vue": "2.x", "vue-server-renderer": "2.x", "vue-template-compiler": "^2.x" diff --git a/packages/server-test-utils/src/renderToString.js b/packages/server-test-utils/src/renderToString.js index 6b27f1cf3..7c02d4138 100644 --- a/packages/server-test-utils/src/renderToString.js +++ b/packages/server-test-utils/src/renderToString.js @@ -6,6 +6,7 @@ import { throwError } from 'shared/util' import { createRenderer } from 'vue-server-renderer' import { mergeOptions } from 'shared/merge-options' import config from './config' +import testUtils from '@vue/test-utils' Vue.config.productionTip = false Vue.config.devtools = false @@ -30,7 +31,8 @@ export default function renderToString ( const vm = createInstance( component, - mergeOptions(options, config) + mergeOptions(options, config), + testUtils.createLocalVue(options.localVue) ) let renderedString = '' diff --git a/packages/test-utils/src/matches.js b/packages/test-utils/src/matches.js index 24f160f1a..c86c531bc 100644 --- a/packages/test-utils/src/matches.js +++ b/packages/test-utils/src/matches.js @@ -43,8 +43,12 @@ export function matches (node, selector) { return element && element.matches && element.matches(selector.value) } - const componentInstance = selector.value.functional - ? node && node[FUNCTIONAL_OPTIONS] + const isFunctionalSelector = typeof selector.value === 'function' + ? selector.value.options.functional + : selector.value.functional + + const componentInstance = isFunctionalSelector + ? node[FUNCTIONAL_OPTIONS] : node.child if (!componentInstance) { diff --git a/packages/test-utils/src/mount.js b/packages/test-utils/src/mount.js index 4a71912aa..fb75e4ee9 100644 --- a/packages/test-utils/src/mount.js +++ b/packages/test-utils/src/mount.js @@ -12,7 +12,7 @@ import { mergeOptions } from 'shared/merge-options' import config from './config' import warnIfNoWindow from './warn-if-no-window' import createWrapper from './create-wrapper' - +import createLocalVue from './create-local-vue' Vue.config.productionTip = false Vue.config.devtools = false @@ -34,7 +34,8 @@ export default function mount ( const parentVm = createInstance( component, - mergedOptions + mergedOptions, + createLocalVue(options.localVue) ) const vm = parentVm.$mount(elm).$refs.vm diff --git a/test/specs/create-local-vue.spec.js b/test/specs/create-local-vue.spec.js index c1aac95f9..d1d88da04 100644 --- a/test/specs/create-local-vue.spec.js +++ b/test/specs/create-local-vue.spec.js @@ -1,6 +1,5 @@ import Vue from 'vue' import Vuex from 'vuex' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' import { createLocalVue } from '~vue/test-utils' import Component from '~resources/components/component.vue' @@ -112,11 +111,6 @@ describeWithShallowAndMount('createLocalVue', mountingMethod => { localVue.use(plugin, pluginOptions) }) - it('installs Vutify successfuly', () => { - const localVue = createLocalVue() - localVue.use(Vuetify) - }) - it('installs plugin into local Vue regardless of previous install in Vue', () => { let installCount = 0 diff --git a/test/specs/external-libraries.spec.js b/test/specs/external-libraries.spec.js new file mode 100644 index 000000000..9027d454e --- /dev/null +++ b/test/specs/external-libraries.spec.js @@ -0,0 +1,17 @@ +import { createLocalVue, mount } from '~vue/test-utils' +import VeeValidate from 'vee-validate' +import { describeWithShallowAndMount } from '~resources/utils' + +describeWithShallowAndMount('external libraries', () => { + it.skip('works with vee validate', () => { + const TestComponent = { + template: '
' + } + const localVue = createLocalVue() + localVue.use(VeeValidate) + const wrapper = mount(TestComponent, { + localVue + }) + wrapper.vm.errors.collect('text') + }) +}) diff --git a/test/specs/mount.spec.js b/test/specs/mount.spec.js index e5ab2c6cd..1a7138529 100644 --- a/test/specs/mount.spec.js +++ b/test/specs/mount.spec.js @@ -6,7 +6,7 @@ import ComponentWithProps from '~resources/components/component-with-props.vue' import ComponentWithMixin from '~resources/components/component-with-mixin.vue' import ComponentAsAClass from '~resources/components/component-as-a-class.vue' import { injectSupported, vueVersion } from '~resources/utils' -import { describeRunIf, itDoNotRunIf } from 'conditional-specs' +import { describeRunIf, itDoNotRunIf, itSkipIf } from 'conditional-specs' import Vuex from 'vuex' describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { @@ -101,6 +101,32 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { expect(wrapper.findAll('div').length).to.equal(1) }) + itSkipIf( + vueVersion < 2.3, + 'handles extended components added to Vue constructor', () => { + const ChildComponent = Vue.extend({ + template: '
', + mounted () { + this.$route.params + } + }) + Vue.component('child-component', ChildComponent) + const TestComponent = { + template: '' + } + let wrapper + try { + wrapper = mount(TestComponent, { + mocks: { + $route: {} + } + }) + } catch (err) {} finally { + delete Vue.options.components['child-component'] + expect(wrapper.find(ChildComponent).exists()).to.equal(true) + } + }) + it('does not use cached component', () => { ComponentWithMixin.methods.someMethod = sinon.stub() mount(ComponentWithMixin) diff --git a/test/specs/wrapper/find.spec.js b/test/specs/wrapper/find.spec.js index f9f088fb0..28aac23a9 100644 --- a/test/specs/wrapper/find.spec.js +++ b/test/specs/wrapper/find.spec.js @@ -344,6 +344,22 @@ describeWithShallowAndMount('find', mountingMethod => { expect(wrapper.find({ ref: 'foo' })).to.be.an('object') }) + itSkipIf(vueVersion < 2.3, + 'returns functional extended component', () => { + const FunctionalExtendedComponent = Vue.extend({ + functional: true, + render: h => h('div') + }) + const TestComponent = { + template: '
', + components: { + FunctionalExtendedComponent + } + } + const wrapper = mountingMethod(TestComponent) + expect(wrapper.find(FunctionalExtendedComponent).exists()).to.equal(true) + }) + it('returns Wrapper of Vue Component matching the extended component', () => { const BaseComponent = { template: '
', diff --git a/yarn.lock b/yarn.lock index 1ddbb3448..8484f5ca9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10788,6 +10788,11 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" integrity sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw== +typescript@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb" + integrity sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" @@ -11139,6 +11144,11 @@ vary@^1.0.0: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +vee-validate@2.1.0-beta.5: + version "2.1.0-beta.5" + resolved "https://registry.yarnpkg.com/vee-validate/-/vee-validate-2.1.0-beta.5.tgz#311c4629face2383964beb06768b379500f25a69" + integrity sha512-XN0FCHBEWKbxKprjzIwIr7ff5U+aFaJZlkoKr1ReuncMwHgG/6iW0hEI4bDuNXx1YKPmm4SyhoXkAOBHIUIwTA== + vendors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" @@ -11370,11 +11380,6 @@ vuepress@^0.14.2: webpackbar "^2.6.1" workbox-build "^3.1.0" -vuetify@^0.16.9: - version "0.16.9" - resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-0.16.9.tgz#fd61f219e4a40d7afe5e24a803df5658a40b38e4" - integrity sha512-HVzdKnMETHINURJhGCMGT3wFw6R8eEHCVcqdC6o/z+eAWNQH7xg1k2hCsKo680lkjiL7tp3vRd9lF+3DpLm/+g== - vuex@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"