From 36e30323cebaf08a4b1419a351e0bcc872b0fb67 Mon Sep 17 00:00:00 2001 From: eddyerburgh Date: Sun, 29 Jul 2018 12:43:09 +0100 Subject: [PATCH 1/8] refactor: remove window dependency from scoped slots --- .../create-instance/create-scoped-slots.js | 32 ++++++---------- .../mounting-options/scopedSlots.spec.js | 38 ++++--------------- 2 files changed, 19 insertions(+), 51 deletions(-) diff --git a/packages/create-instance/create-scoped-slots.js b/packages/create-instance/create-scoped-slots.js index 6211fd320..d77ba2ab2 100644 --- a/packages/create-instance/create-scoped-slots.js +++ b/packages/create-instance/create-scoped-slots.js @@ -36,25 +36,12 @@ function getVueTemplateCompilerHelpers (): { [name: string]: Function } { } function validateEnvironment (): void { - if (window.navigator.userAgent.match(/PhantomJS/i)) { - throwError( - `the scopedSlots option does not support PhantomJS. ` + - `Please use Puppeteer, or pass a component.` - ) - } if (vueVersion < 2.5) { throwError(`the scopedSlots option is only supported in ` + `vue@2.5+.`) } } -function validateTempldate (template: string): void { - if (template.trim().substr(0, 9) === ']+ slot-scope=\"(.+)\"/ export default function createScopedSlots ( scopedSlotsOption: ?{ [slotName: string]: string } @@ -67,13 +54,18 @@ export default function createScopedSlots ( const helpers = getVueTemplateCompilerHelpers() for (const name in scopedSlotsOption) { const template = scopedSlotsOption[name] - validateTempldate(template) const render = compileToFunctions(template).render - const domParser = new window.DOMParser() - const _document = domParser.parseFromString(template, 'text/html') - const slotScope = _document.body.firstChild.getAttribute( - 'slot-scope' - ) + const match = template.match(scopedSlotRe) + + if (!match) { + throwError( + `the root tag in a scopedSlot template must have a ` + + `slot-scope attribute` + ) + } + + // $FlowIgnore + const slotScope = match[1] const isDestructuring = isDestructuringSlotScope(slotScope) scopedSlots[name] = function (props) { if (isDestructuring) { diff --git a/test/specs/mounting-options/scopedSlots.spec.js b/test/specs/mounting-options/scopedSlots.spec.js index 05259800d..8b7d44b20 100644 --- a/test/specs/mounting-options/scopedSlots.spec.js +++ b/test/specs/mounting-options/scopedSlots.spec.js @@ -14,7 +14,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { }) itDoNotRunIf( - vueVersion < 2.5 || isRunningPhantomJS, + vueVersion < 2.5, 'mounts component scoped slots in render function', () => { const destructuringWrapper = mountingMethod( @@ -56,7 +56,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { ) itDoNotRunIf( - vueVersion < 2.5 || isRunningPhantomJS, + vueVersion < 2.5, 'mounts component scoped slots', () => { const wrapper = mountingMethod(ComponentWithScopedSlots, { @@ -64,7 +64,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { scopedSlots: { destructuring: '

{{index}},{{item}}

', - list: '

{{foo.index}},{{foo.text}}

', + list: '', single: '

{{bar.text}}

', noProps: '

baz

' } @@ -105,25 +105,6 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { } ) - itDoNotRunIf( - vueVersion < 2.5 || isRunningPhantomJS, - 'throws exception when it is seted to a template tag at top', - () => { - const fn = () => { - mountingMethod(ComponentWithScopedSlots, { - scopedSlots: { - single: '' - } - }) - } - const message = - '[vue-test-utils]: the scopedSlots option does not support a template tag as the root element.' - expect(fn) - .to.throw() - .with.property('message', message) - } - ) - itDoNotRunIf( vueVersion >= 2.5 || isRunningPhantomJS, 'throws exception when vue version < 2.5', @@ -143,23 +124,18 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { } ) - itDoNotRunIf( - vueVersion < 2.5, - 'throws exception when using PhantomJS', + it( + 'throws exception when template does not include slot-scope attribute', () => { - if (window.navigator.userAgent.match(/Chrome|PhantomJS/i)) { - return - } - window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign const fn = () => { mountingMethod(ComponentWithScopedSlots, { scopedSlots: { - list: '

{{foo.index}},{{foo.text}}

' + list: '

{{foo.index}},{{foo.text}}

' } }) } const message = - '[vue-test-utils]: the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.' + '[vue-test-utils]: the root tag in a scopedSlot template must have a slot-scope attribute' expect(fn) .to.throw() .with.property('message', message) From 94fbb26d56e58499626656bdf9e48f9201081e67 Mon Sep 17 00:00:00 2001 From: eddyerburgh Date: Sun, 29 Jul 2018 14:27:33 +0100 Subject: [PATCH 2/8] feat: support JSX scoped slots --- .babelrc | 2 +- package.json | 1 + .../create-instance/create-scoped-slots.js | 39 ++++++------ .../mounting-options/scopedSlots.spec.js | 63 +++++++++++-------- yarn.lock | 6 ++ 5 files changed, 64 insertions(+), 47 deletions(-) diff --git a/.babelrc b/.babelrc index 9f1cf787f..4d755394a 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { "presets": ["env", "stage-2", "flow-vue"], - "plugins": ["transform-decorators-legacy"], + "plugins": ["transform-decorators-legacy", "transform-vue-jsx"], "comments": false } diff --git a/package.json b/package.json index 91a211ea3..596e39545 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "babel-eslint": "^8.2.2", "babel-loader": "^7.1.3", "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-vue-jsx": "^3.7.0", "babel-polyfill": "^6.23.0", "babel-preset-env": "^1.6.0", "babel-preset-flow-vue": "^1.0.0", diff --git a/packages/create-instance/create-scoped-slots.js b/packages/create-instance/create-scoped-slots.js index d77ba2ab2..777bb9f70 100644 --- a/packages/create-instance/create-scoped-slots.js +++ b/packages/create-instance/create-scoped-slots.js @@ -32,19 +32,20 @@ function getVueTemplateCompilerHelpers (): { [name: string]: Function } { names.forEach(name => { helpers[name] = vue._renderProxy[name] }) + helpers.$createElement = vue._renderProxy.$createElement return helpers } function validateEnvironment (): void { - if (vueVersion < 2.5) { - throwError(`the scopedSlots option is only supported in ` + `vue@2.5+.`) + if (vueVersion < 2.1) { + throwError(`the scopedSlots option is only supported in ` + `vue@2.1+.`) } } const scopedSlotRe = /<[^>]+ slot-scope=\"(.+)\"/ export default function createScopedSlots ( - scopedSlotsOption: ?{ [slotName: string]: string } + scopedSlotsOption: ?{ [slotName: string]: string | Function } ): { [slotName: string]: (props: Object) => VNode | Array} { const scopedSlots = {} if (!scopedSlotsOption) { @@ -52,26 +53,24 @@ export default function createScopedSlots ( } validateEnvironment() const helpers = getVueTemplateCompilerHelpers() - for (const name in scopedSlotsOption) { - const template = scopedSlotsOption[name] - const render = compileToFunctions(template).render - const match = template.match(scopedSlotRe) + for (const s in scopedSlotsOption) { + const slot = scopedSlotsOption[s] + const isFn = typeof slot === 'function' + const renderFn = isFn + ? slot + : compileToFunctions(slot).renderFn - if (!match) { - throwError( - `the root tag in a scopedSlot template must have a ` + - `slot-scope attribute` - ) - } + const hasSlotScopeAttr = !isFn && slot.match(scopedSlotRe) + // // $FlowIgnore + const slotScope = hasSlotScopeAttr && hasSlotScopeAttr[1] - // $FlowIgnore - const slotScope = match[1] - const isDestructuring = isDestructuringSlotScope(slotScope) - scopedSlots[name] = function (props) { - if (isDestructuring) { - return render.call({ ...helpers, ...props }) + scopedSlots[s] = function (props) { + if (isFn) { + return renderFn.call({ ...helpers }, props) + } else if (slotScope && !isDestructuringSlotScope(slotScope)) { + return renderFn.call({ ...helpers, [slotScope]: props }) } else { - return render.call({ ...helpers, [slotScope]: props }) + return renderFn.call({ ...helpers, ...props }) } } } diff --git a/test/specs/mounting-options/scopedSlots.spec.js b/test/specs/mounting-options/scopedSlots.spec.js index 8b7d44b20..c1513329a 100644 --- a/test/specs/mounting-options/scopedSlots.spec.js +++ b/test/specs/mounting-options/scopedSlots.spec.js @@ -1,7 +1,6 @@ import { describeWithShallowAndMount, - vueVersion, - isRunningPhantomJS + vueVersion } from '~resources/utils' import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue' import { itDoNotRunIf } from 'conditional-specs' @@ -38,7 +37,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { const notDestructuringWrapper = mountingMethod( { render: function () { - return this.$scopedSlots.default({ + return this.$scopedSlots.named({ index: 1, item: 'foo' }) @@ -46,7 +45,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { }, { scopedSlots: { - default: + named: '

{{props.index}},{{props.item}}

' } } @@ -64,7 +63,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { scopedSlots: { destructuring: '

{{index}},{{item}}

', - list: '', + list: '', single: '

{{bar.text}}

', noProps: '

baz

' } @@ -105,37 +104,49 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { } ) - itDoNotRunIf( - vueVersion >= 2.5 || isRunningPhantomJS, - 'throws exception when vue version < 2.5', - () => { - const fn = () => { - mountingMethod(ComponentWithScopedSlots, { - scopedSlots: { - list: '

{{foo.index}},{{foo.text}}

' - } - }) + it('handles JSX', () => { + const wrapper = mountingMethod({ + template: '
', + data: () => ({ + foo: 'bar' + }) + }, { + scopedSlots: { + single ({ text }) { + return

{{ text }}

+ } } - const message = - '[vue-test-utils]: the scopedSlots option is only supported in vue@2.5+.' - expect(fn) - .to.throw() - .with.property('message', message) - } - ) + }) + expect(wrapper.html()).to.equal('

bar

') + }) - it( - 'throws exception when template does not include slot-scope attribute', + it('handles no slot-scope', () => { + const wrapper = mountingMethod({ + template: '
', + data: () => ({ + foo: 'bar' + }) + }, { + scopedSlots: { + single: '

{{text}},{{i}}

' + } + }) + expect(wrapper.html()).to.equal('

bar,123

') + }) + + itDoNotRunIf( + vueVersion > 2.0, + 'throws exception when vue version < 2.1', () => { const fn = () => { mountingMethod(ComponentWithScopedSlots, { scopedSlots: { - list: '

{{foo.index}},{{foo.text}}

' + list: '

{{foo.index}},{{foo.text}}

' } }) } const message = - '[vue-test-utils]: the root tag in a scopedSlot template must have a slot-scope attribute' + '[vue-test-utils]: the scopedSlots option is only supported in vue@2.1+.' expect(fn) .to.throw() .with.property('message', message) diff --git a/yarn.lock b/yarn.lock index b78e59e32..e8333a4cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1790,6 +1790,12 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" +babel-plugin-transform-vue-jsx@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-3.7.0.tgz#d40492e6692a36b594f7e9a1928f43e969740960" + dependencies: + esutils "^2.0.2" + babel-plugin-transform-vue-jsx@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-4.0.1.tgz#2c8bddce87a6ef09eaa59869ff1bfbeeafc5f88d" From 998b77b843243f19e2fdc0ccd037dc62810f18b1 Mon Sep 17 00:00:00 2001 From: eddyerburgh Date: Sun, 29 Jul 2018 14:43:08 +0100 Subject: [PATCH 3/8] refactor: rename functions --- flow/options.flow.js | 2 +- packages/create-instance/create-scoped-slots.js | 17 +++++++++-------- test/specs/mounting-options/scopedSlots.spec.js | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/flow/options.flow.js b/flow/options.flow.js index d154c1f14..c95fb78bf 100644 --- a/flow/options.flow.js +++ b/flow/options.flow.js @@ -5,7 +5,7 @@ declare type Options = { mocks?: Object, methods?: { [key: string]: Function }, slots?: SlotsObject, - scopedSlots?: { [key: string]: string }, + scopedSlots?: { [key: string]: string | Function }, localVue?: Component, provide?: Object, stubs?: Stubs, diff --git a/packages/create-instance/create-scoped-slots.js b/packages/create-instance/create-scoped-slots.js index 777bb9f70..5f4138616 100644 --- a/packages/create-instance/create-scoped-slots.js +++ b/packages/create-instance/create-scoped-slots.js @@ -38,15 +38,17 @@ function getVueTemplateCompilerHelpers (): { [name: string]: Function } { function validateEnvironment (): void { if (vueVersion < 2.1) { - throwError(`the scopedSlots option is only supported in ` + `vue@2.1+.`) + throwError(`the scopedSlots option is only supported in vue@2.1+.`) } } -const scopedSlotRe = /<[^>]+ slot-scope=\"(.+)\"/ +const slotScopeRe = /<[^>]+ slot-scope=\"(.+)\"/ export default function createScopedSlots ( scopedSlotsOption: ?{ [slotName: string]: string | Function } -): { [slotName: string]: (props: Object) => VNode | Array} { +): { + [slotName: string]: (props: Object) => VNode | Array +} { const scopedSlots = {} if (!scopedSlotsOption) { return scopedSlots @@ -56,14 +58,13 @@ export default function createScopedSlots ( for (const s in scopedSlotsOption) { const slot = scopedSlotsOption[s] const isFn = typeof slot === 'function' - const renderFn = isFn + // Type check in render function to silence flow + const renderFn = typeof slot === 'function' ? slot - : compileToFunctions(slot).renderFn + : compileToFunctions(slot).render - const hasSlotScopeAttr = !isFn && slot.match(scopedSlotRe) - // // $FlowIgnore + const hasSlotScopeAttr = !isFn && slot.match(slotScopeRe) const slotScope = hasSlotScopeAttr && hasSlotScopeAttr[1] - scopedSlots[s] = function (props) { if (isFn) { return renderFn.call({ ...helpers }, props) diff --git a/test/specs/mounting-options/scopedSlots.spec.js b/test/specs/mounting-options/scopedSlots.spec.js index c1513329a..58a7c3cd5 100644 --- a/test/specs/mounting-options/scopedSlots.spec.js +++ b/test/specs/mounting-options/scopedSlots.spec.js @@ -13,7 +13,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { }) itDoNotRunIf( - vueVersion < 2.5, + vueVersion < 2.1, 'mounts component scoped slots in render function', () => { const destructuringWrapper = mountingMethod( @@ -55,7 +55,7 @@ describeWithShallowAndMount('scopedSlots', mountingMethod => { ) itDoNotRunIf( - vueVersion < 2.5, + vueVersion < 2.1, 'mounts component scoped slots', () => { const wrapper = mountingMethod(ComponentWithScopedSlots, { From 95c061f055150f6936525c0d7ac48c827dc7750e Mon Sep 17 00:00:00 2001 From: eddyerburgh Date: Sun, 29 Jul 2018 16:24:37 +0100 Subject: [PATCH 4/8] refactor: handle template as root element --- docs/api/options.md | 48 +++++++--- .../create-instance/create-scoped-slots.js | 26 ++++-- .../mounting-options/scopedSlots.spec.js | 92 +++++++++++++------ 3 files changed, 119 insertions(+), 47 deletions(-) diff --git a/docs/api/options.md b/docs/api/options.md index 1707e78fb..2ace5a6bd 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -69,30 +69,52 @@ expect(wrapper.find('div')).toBe(true) ## scopedSlots -- type: `{ [name: string]: string }` +- type: `{ [name: string]: string|Function }` -Provide an object of scoped slots contents to the component. The key corresponds to the slot name. The value can be a template string. +Provide an object of scoped slots to the component. The key corresponds to the slot name. -There are three limitations. +You can set the name of the props using the slot-scope attribute: -* This option is only supported in vue@2.5+. +```js +shallowMount(Component, { + scopedSlots: { + foo: '

{{foo.index}},{{foo.text}}

' + } +}) +``` -* You can not use `