From d8485f5ef277f570a0f6b59b7d28b90e7dcd39ad Mon Sep 17 00:00:00 2001 From: 38elements <38elements@users.noreply.github.com> Date: Sun, 6 May 2018 00:54:09 +0900 Subject: [PATCH] fix: function names (#580) * Revert "refactor: refactor stub-components.js (#544)" This reverts commit db5e07edc31f0a77280e435b492bbd3ddcbf73f4. * Revert "refactor: refactor add-slots.js (#556)" This reverts commit ce9e1bf81383995d4338e7e5c3ed6ed840a3ba6f. * delete packages/test-utils/src/shallow.js * add functions * add type SlotValue * add test * fix slotValue * fix validateEnvironment * add test * fix lint * Update slots.spec.js --- flow/vue.flow.js | 1 + packages/create-instance/add-slots.js | 62 +++++-- packages/create-instance/validate-slots.js | 33 ++-- packages/shared/compile-template.js | 15 +- packages/shared/stub-components-validate.js | 44 ----- packages/shared/stub-components.js | 183 +++++++++++++++----- packages/shared/util.js | 8 - packages/test-utils/src/shallow-mount.js | 4 +- packages/test-utils/src/shallow.js | 36 ---- test/specs/mounting-options/slots.spec.js | 14 ++ 10 files changed, 227 insertions(+), 173 deletions(-) delete mode 100644 packages/shared/stub-components-validate.js delete mode 100644 packages/test-utils/src/shallow.js diff --git a/flow/vue.flow.js b/flow/vue.flow.js index d84644092..d7a51d41f 100644 --- a/flow/vue.flow.js +++ b/flow/vue.flow.js @@ -4,3 +4,4 @@ declare type Component = Object // eslint-disable-line no-undef declare type VNode = Object // eslint-disable-line no-undef +declare type SlotValue = Component | string | Array | Array diff --git a/packages/create-instance/add-slots.js b/packages/create-instance/add-slots.js index 82a1c4391..9b802ceaf 100644 --- a/packages/create-instance/add-slots.js +++ b/packages/create-instance/add-slots.js @@ -1,18 +1,21 @@ // @flow import { compileToFunctions } from 'vue-template-compiler' +import { throwError } from 'shared/util' import { validateSlots } from './validate-slots' -import { toArray } from 'shared/util' -function isSingleHTMLTag (template: string) { - if (!template.startsWith('<') || !template.endsWith('>')) { +function isSingleElement (slotValue: string): boolean { + const _slotValue = slotValue.trim() + if (_slotValue[0] !== '<' || _slotValue[_slotValue.length - 1] !== '>') { return false } - const _document = new window.DOMParser().parseFromString(template, 'text/html') + const domParser = new window.DOMParser() + const _document = domParser.parseFromString(slotValue, 'text/html') return _document.body.childElementCount === 1 } -function createElementFromAdvancedString (slotValue, vm) { +// see https://github.com/vuejs/vue-test-utils/pull/274 +function createVNodes (vm: Component, slotValue: string) { const compiledResult = compileToFunctions(`
${slotValue}{{ }}
`) const _staticRenderFns = vm._renderProxy.$options.staticRenderFns vm._renderProxy.$options.staticRenderFns = compiledResult.staticRenderFns @@ -21,23 +24,54 @@ function createElementFromAdvancedString (slotValue, vm) { return elem } -function createElement (slotValue: string | Object, vm) { +function validateEnvironment (): void { + if (!compileToFunctions) { + throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined') + } + if (typeof window === 'undefined') { + throwError('the slots string option does not support strings in server-test-uitls.') + } + if (window.navigator.userAgent.match(/PhantomJS/i)) { + throwError('the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.') + } +} + +function addSlotToVm (vm: Component, slotName: string, slotValue: SlotValue): void { + let elem if (typeof slotValue === 'string') { - slotValue = slotValue.trim() - if (isSingleHTMLTag(slotValue)) { - return vm.$createElement(compileToFunctions(slotValue)) + validateEnvironment() + if (isSingleElement(slotValue)) { + elem = vm.$createElement(compileToFunctions(slotValue)) } else { - return createElementFromAdvancedString(slotValue, vm) + elem = createVNodes(vm, slotValue) } } else { - return vm.$createElement(slotValue) + elem = vm.$createElement(slotValue) + } + if (Array.isArray(elem)) { + if (Array.isArray(vm.$slots[slotName])) { + vm.$slots[slotName] = [...vm.$slots[slotName], ...elem] + } else { + vm.$slots[slotName] = [...elem] + } + } else { + if (Array.isArray(vm.$slots[slotName])) { + vm.$slots[slotName].push(elem) + } else { + vm.$slots[slotName] = [elem] + } } } export function addSlots (vm: Component, slots: Object): void { validateSlots(slots) - Object.keys(slots).forEach(name => { - vm.$slots[name] = toArray(slots[name]) - .map(slotValue => createElement(slotValue, vm)) + Object.keys(slots).forEach((key) => { + if (Array.isArray(slots[key])) { + slots[key].forEach((slotValue) => { + addSlotToVm(vm, key, slotValue) + }) + } else { + addSlotToVm(vm, key, slots[key]) + } }) } diff --git a/packages/create-instance/validate-slots.js b/packages/create-instance/validate-slots.js index 0e69ca20e..1c1d7dd9d 100644 --- a/packages/create-instance/validate-slots.js +++ b/packages/create-instance/validate-slots.js @@ -1,26 +1,23 @@ // @flow -import { throwError, toArray, isObject } from 'shared/util' -import { compileToFunctions } from 'vue-template-compiler' +import { throwError } from 'shared/util' + +function isValidSlot (slot: any): boolean { + return Array.isArray(slot) || (slot !== null && typeof slot === 'object') || typeof slot === 'string' +} export function validateSlots (slots: Object): void { - Object.keys(slots).forEach(key => { - toArray(slots[key]).forEach(slotValue => { - if (!isObject(slotValue) && typeof slotValue !== 'string') { - throwError('slots[key] must be a Component, string or an array of Components') - } + slots && Object.keys(slots).forEach((key) => { + if (!isValidSlot(slots[key])) { + throwError('slots[key] must be a Component, string or an array of Components') + } - if (typeof slotValue === 'string') { - if (!compileToFunctions) { - throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined') - } - if (typeof window === 'undefined') { - throwError('the slots string option does not support strings in server-test-uitls.') - } - if (window.navigator.userAgent.match(/PhantomJS/i)) { - throwError('the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.') + if (Array.isArray(slots[key])) { + slots[key].forEach((slotValue) => { + if (!isValidSlot(slotValue)) { + throwError('slots[key] must be a Component, string or an array of Components') } - } - }) + }) + } }) } diff --git a/packages/shared/compile-template.js b/packages/shared/compile-template.js index 530a24dde..b449bffbc 100644 --- a/packages/shared/compile-template.js +++ b/packages/shared/compile-template.js @@ -3,13 +3,14 @@ import { compileToFunctions } from 'vue-template-compiler' export function compileTemplate (component: Component) { - Object.keys(component.components || {}).forEach((c) => { - const cmp = component.components[c] - if (!cmp.render) { - compileTemplate(cmp) - } - }) - + if (component.components) { + Object.keys(component.components).forEach((c) => { + const cmp = component.components[c] + if (!cmp.render) { + compileTemplate(cmp) + } + }) + } if (component.extends) { compileTemplate(component.extends) } diff --git a/packages/shared/stub-components-validate.js b/packages/shared/stub-components-validate.js deleted file mode 100644 index 81d49b6de..000000000 --- a/packages/shared/stub-components-validate.js +++ /dev/null @@ -1,44 +0,0 @@ -// @flow - -import { throwError } from './util' -import { compileToFunctions } from 'vue-template-compiler' - -export function validateStubOptions (stubOptions: Array | Object) { - if (Array.isArray(stubOptions)) { - if (containsNonStringItem(stubOptions)) { - throwError('each item in an options.stubs array must be a string') - } - } else { - if (containsInvalidOptions(stubOptions)) { - throwError('options.stub values must be passed a string or component') - } - - if (necessaryCompileToFunctionsMissed(stubOptions)) { - throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined') - } - } -} - -function containsNonStringItem (array: Array): boolean { - return array.some(name => typeof name !== 'string') -} - -function necessaryCompileToFunctionsMissed (stubOptions: Object): boolean { - return Object.keys(stubOptions) - .map(key => stubOptions[key]) - .some(stub => typeof stub === 'string') && !compileToFunctions -} - -function containsInvalidOptions (stubOptions: Object): boolean { - return Object.keys(stubOptions) - .map(key => stubOptions[key]) - .some(isInvalidStubOption) -} - -function isInvalidStubOption (stub): boolean { - return !['string', 'boolean'].includes(typeof stub) && !isVueComponent(stub) -} - -function isVueComponent (cmp) { - return cmp && (cmp.render || cmp.template || cmp.options) -} diff --git a/packages/shared/stub-components.js b/packages/shared/stub-components.js index ebf305f14..fda90dc37 100644 --- a/packages/shared/stub-components.js +++ b/packages/shared/stub-components.js @@ -1,12 +1,28 @@ // @flow +import Vue from 'vue' import { compileToFunctions } from 'vue-template-compiler' -import { validateStubOptions } from 'shared/stub-components-validate' -import { componentNeedsCompiling } from 'shared/validators' -import { compileTemplate } from 'shared/compile-template' +import { throwError } from './util' +import { componentNeedsCompiling } from './validators' +import { compileTemplate } from './compile-template' +import { capitalize, camelize, hyphenate } from './util' + +function isVueComponent (comp) { + return comp && (comp.render || comp.template || comp.options) +} + +function isValidStub (stub: any) { + return !!stub && + typeof stub === 'string' || + (stub === true) || + (isVueComponent(stub)) +} + +function isRequiredComponent (name) { + return name === 'KeepAlive' || name === 'Transition' || name === 'TransitionGroup' +} function getCoreProperties (component: Component): Object { - if (!component) return {} return { attrs: component.attrs, name: component.name, @@ -24,68 +40,147 @@ function getCoreProperties (component: Component): Object { functional: component.functional } } +function createStubFromString (templateString: string, originalComponent: Component): Object { + if (!compileToFunctions) { + throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined') + } + + if (templateString.indexOf(hyphenate(originalComponent.name)) !== -1 || + templateString.indexOf(capitalize(originalComponent.name)) !== -1 || + templateString.indexOf(camelize(originalComponent.name)) !== -1) { + throwError('options.stub cannot contain a circular reference') + } -function createStubFromString (originalComponent: Component, template: string): Object { return { ...getCoreProperties(originalComponent), - ...compileToFunctions(template) + ...compileToFunctions(templateString) } } -function createBlankStub (originalComponent: Component): Object { +function createBlankStub (originalComponent: Component) { return { ...getCoreProperties(originalComponent), render: h => h('') } } -function createStubFromComponent (component: Component, name: string): Object { - if (componentNeedsCompiling(component)) compileTemplate(component) - return name ? { ...component, name } : component -} +export function createComponentStubs (originalComponents: Object = {}, stubs: Object): Object { + const components = {} + if (!stubs) { + return components + } + if (Array.isArray(stubs)) { + stubs.forEach(stub => { + if (stub === false) { + return + } -function createStub (originalComponent: Component, stubValue): Object { - if (stubValue === true) { - return createBlankStub(originalComponent) - } else if (typeof stubValue === 'string') { - return createStubFromString(originalComponent, stubValue) + if (typeof stub !== 'string') { + throwError('each item in an options.stubs array must be a string') + } + components[stub] = createBlankStub({}) + }) } else { - return createStubFromComponent(stubValue, originalComponent && originalComponent.name) + Object.keys(stubs).forEach(stub => { + if (stubs[stub] === false) { + return + } + if (!isValidStub(stubs[stub])) { + throwError('options.stub values must be passed a string or component') + } + if (stubs[stub] === true) { + components[stub] = createBlankStub({}) + return + } + + if (componentNeedsCompiling(stubs[stub])) { + compileTemplate(stubs[stub]) + } + + if (originalComponents[stub]) { + // Remove cached constructor + delete originalComponents[stub]._Ctor + if (typeof stubs[stub] === 'string') { + components[stub] = createStubFromString(stubs[stub], originalComponents[stub]) + } else { + components[stub] = { + ...stubs[stub], + name: originalComponents[stub].name + } + } + } else { + if (typeof stubs[stub] === 'string') { + if (!compileToFunctions) { + throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined') + } + components[stub] = { + ...compileToFunctions(stubs[stub]) + } + } else { + components[stub] = { + ...stubs[stub] + } + } + } + // ignoreElements does not exist in Vue 2.0.x + if (Vue.config.ignoredElements) { + Vue.config.ignoredElements.push(stub) + } + }) } + return components +} + +function stubComponents (components: Object, stubbedComponents: Object) { + Object.keys(components).forEach(component => { + // Remove cached constructor + delete components[component]._Ctor + if (!components[component].name) { + components[component].name = component + } + stubbedComponents[component] = createBlankStub(components[component]) + + // ignoreElements does not exist in Vue 2.0.x + if (Vue.config.ignoredElements) { + Vue.config.ignoredElements.push(component) + } + }) } -function normalizeStubOptions (components: ?Object, stubOptions: ?Object | Array): Object { - if (!stubOptions) { - stubOptions = Object.keys(components || {}) +export function createComponentStubsForAll (component: Component): Object { + const stubbedComponents = {} + + if (component.components) { + stubComponents(component.components, stubbedComponents) } - if (Array.isArray(stubOptions)) { - stubOptions = stubOptions.reduce((object, name) => { - object[name] = true - return object - }, {}) + + let extended = component.extends + + // Loop through extended component chains to stub all child components + while (extended) { + if (extended.components) { + stubComponents(extended.components, stubbedComponents) + } + extended = extended.extends } - return stubOptions -} -export function createComponentStubs (components: Object = {}, stubOptions: Object): Object { - validateStubOptions(stubOptions) - return createStubs(components, stubOptions) -} + if (component.extendOptions && component.extendOptions.components) { + stubComponents(component.extendOptions.components, stubbedComponents) + } -export function createComponentStubsForAll (component: Component, stubs: Object = {}): Object { - if (!component) return stubs - Object.assign(stubs, createStubs(component.components)) - return createComponentStubsForAll(component.extends || component.extendOptions, stubs) + return stubbedComponents } -export function createStubs (components: Object, stubOptions: ?Object | Array): Object { - const options: Object = normalizeStubOptions(components, stubOptions) +export function createComponentStubsForGlobals (instance: Component): Object { + const components = {} + Object.keys(instance.options.components).forEach((c) => { + if (isRequiredComponent(c)) { + return + } - return Object.keys(options) - .filter(name => !['KeepAlive', 'Transition', 'TransitionGroup'].includes(name)) - .filter(name => options[name] !== false) - .reduce((stubs, name) => { - stubs[name] = createStub(components[name], options[name]) - return stubs - }, {}) + components[c] = createBlankStub(instance.options.components[c]) + delete instance.options.components[c]._Ctor // eslint-disable-line no-param-reassign + delete components[c]._Ctor // eslint-disable-line no-param-reassign + }) + return components } diff --git a/packages/shared/util.js b/packages/shared/util.js index 6da2418b8..c68044467 100644 --- a/packages/shared/util.js +++ b/packages/shared/util.js @@ -21,11 +21,3 @@ export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.sli */ const hyphenateRE = /\B([A-Z])/g export const hyphenate = (str: string) => str.replace(hyphenateRE, '-$1').toLowerCase() - -export function toArray (value: any) { - return Array.isArray(value) ? value : [value] -} - -export function isObject (obj: mixed): boolean %checks { - return obj !== null && typeof obj === 'object' -} diff --git a/packages/test-utils/src/shallow-mount.js b/packages/test-utils/src/shallow-mount.js index 55d878bad..037fd0720 100644 --- a/packages/test-utils/src/shallow-mount.js +++ b/packages/test-utils/src/shallow-mount.js @@ -6,7 +6,7 @@ import mount from './mount' import type VueWrapper from './vue-wrapper' import { createComponentStubsForAll, - createStubs + createComponentStubsForGlobals } from 'shared/stub-components' import { camelize, capitalize, @@ -29,7 +29,7 @@ export default function shallowMount ( return mount(component, { ...options, components: { - ...createStubs(vue.options.components), + ...createComponentStubsForGlobals(vue), ...createComponentStubsForAll(component) } }) diff --git a/packages/test-utils/src/shallow.js b/packages/test-utils/src/shallow.js deleted file mode 100644 index 706782d72..000000000 --- a/packages/test-utils/src/shallow.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow - -import './warn-if-no-window' -import Vue from 'vue' -import mount from './mount' -import type VueWrapper from './vue-wrapper' -import { - createComponentStubsForAll, - createStubs -} from 'shared/stub-components' -import { camelize, - capitalize, - hyphenate -} from 'shared/util' - -export default function shallow ( - component: Component, - options: Options = {} -): VueWrapper { - const vue = options.localVue || Vue - - // remove any recursive components added to the constructor - // in vm._init from previous tests - if (component.name && component.components) { - delete component.components[capitalize(camelize(component.name))] - delete component.components[hyphenate(component.name)] - } - - return mount(component, { - ...options, - components: { - ...createStubs(vue.options.components), - ...createComponentStubsForAll(component) - } - }) -} diff --git a/test/specs/mounting-options/slots.spec.js b/test/specs/mounting-options/slots.spec.js index 9b4803395..5bdf898c4 100644 --- a/test/specs/mounting-options/slots.spec.js +++ b/test/specs/mounting-options/slots.spec.js @@ -75,6 +75,18 @@ describeWithMountingMethods('options.slots', (mountingMethod) => { } }) + itDoNotRunIf( + typeof window === 'undefined' || window.navigator.userAgent.match(/Chrome/i), + 'works if the UserAgent is PhantomJS when passed Component is in slot object', () => { + window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign + const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: [Component] }}) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.contains(Component)).to.equal(true) + } + }) + itDoNotRunIf( typeof window === 'undefined' || window.navigator.userAgent.match(/Chrome/i), 'throws error if the UserAgent is PhantomJS when passed string is in slot object', () => { @@ -113,6 +125,8 @@ describeWithMountingMethods('options.slots', (mountingMethod) => { expect(wrapper6.find('main').html()).to.equal('

1

2

') const wrapper7 = mountingMethod(ComponentWithSlots, { slots: { default: '1

2

3' }}) expect(wrapper7.find('main').html()).to.equal('
1

2

3
') + const wrapper8 = mountingMethod(ComponentWithSlots, { slots: { default: ' space ' }}) + expect(wrapper8.find('main').html()).to.equal('
space
') }) itSkipIf(mountingMethod.name === 'renderToString',