Skip to content

Commit

Permalink
feat: add scoped slots option (#507)
Browse files Browse the repository at this point in the history
  • Loading branch information
38elements authored and eddyerburgh committed Apr 14, 2018
1 parent fa45baf commit e6a54b2
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/en/README.md
Expand Up @@ -23,6 +23,7 @@ Vue Test Utils is the official unit testing utility library for Vue.js.
* [Mounting Options](api/options.md)
- [context](api/options.md#context)
- [slots](api/options.md#slots)
- [scopedSlots](api/options.md#scopedslots)
- [stubs](api/options.md#stubs)
- [mocks](api/options.md#mocks)
- [localVue](api/options.md#localvue)
Expand Down
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Expand Up @@ -19,6 +19,7 @@
* [Mounting Options](api/options.md)
- [context](api/options.md#context)
- [slots](api/options.md#slots)
- [scopedSlots](api/options.md#scopedslots)
- [stubs](api/options.md#stubs)
- [mocks](api/options.md#mocks)
- [localVue](api/options.md#localvue)
Expand Down
1 change: 1 addition & 0 deletions docs/en/api/README.md
Expand Up @@ -7,6 +7,7 @@
* [Mounting Options](./options.md)
- [context](./options.md#context)
- [slots](./options.md#slots)
- [scopedSlots](./options.md#scopedslots)
- [stubs](./options.md#stubs)
- [mocks](./options.md#mocks)
- [localVue](./options.md#localvue)
Expand Down
29 changes: 28 additions & 1 deletion docs/en/api/options.md
Expand Up @@ -6,6 +6,7 @@ Options for `mount` and `shallow`. The options object can contain both Vue Test

- [`context`](#context)
- [`slots`](#slots)
- [`scopedSlots`](#scopedslots)
- [`stubs`](#stubs)
- [`mocks`](#mocks)
- [`localVue`](#localvue)
Expand Down Expand Up @@ -66,7 +67,33 @@ You can pass text to `slots`.
There is a limitation to this.

This does not support PhantomJS.
Please use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer).
You can use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer) as an alternative.

### `scopedSlots`

- type: `{ [name: string]: string }`

Provide an object of scoped slots contents to the component. The key corresponds to the slot name. The value can be a template string.

There are three limitations.

* This option is only supported in vue@2.5+.

* You can not use `<template>` tag as the root element in the `scopedSlots` option.

* This does not support PhantomJS.
You can use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer) as an alternative.

Example:

```js
const wrapper = shallow(Component, {
scopedSlots: {
foo: '<p slot-scope="props">{{props.index}},{{props.text}}</p>'
}
})
expect(wrapper.find('#fooWrapper').html()).toBe('<div id="fooWrapper"><p>0,text1</p><p>1,text2</p><p>2,text3</p></div>')
```

### `stubs`

Expand Down
1 change: 1 addition & 0 deletions flow/options.flow.js
Expand Up @@ -2,6 +2,7 @@ declare type Options = { // eslint-disable-line no-undef
attachToDocument?: boolean,
mocks?: Object,
slots?: Object,
scopedSlots?: Object,
localVue?: Component,
provide?: Object,
stubs?: Object,
Expand Down
17 changes: 17 additions & 0 deletions packages/create-instance/add-scoped-slots.js
@@ -0,0 +1,17 @@
// @flow

import { compileToFunctions } from 'vue-template-compiler'
import { throwError } from 'shared/util'

export function addScopedSlots (vm: Component, scopedSlots: Object): void {
Object.keys(scopedSlots).forEach((key) => {
const template = scopedSlots[key].trim()
if (template.substr(0, 9) === '<template') {
throwError('the scopedSlots option does not support a template tag as the root element.')
}
const domParser = new window.DOMParser()
const _document = domParser.parseFromString(template, 'text/html')
vm.$_vueTestUtils_scopedSlots[key] = compileToFunctions(template).render
vm.$_vueTestUtils_slotScopes[key] = _document.body.firstChild.getAttribute('slot-scope')
})
}
6 changes: 3 additions & 3 deletions packages/create-instance/add-slots.js
Expand Up @@ -11,12 +11,12 @@ function addSlotToVm (vm: Component, slotName: string, slotValue: Component | st
throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined')
}
if (window.navigator.userAgent.match(/PhantomJS/i)) {
throwError('option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component')
throwError('the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.')
}
const domParser = new window.DOMParser()
const document = domParser.parseFromString(slotValue, 'text/html')
const _document = domParser.parseFromString(slotValue, 'text/html')
const _slotValue = slotValue.trim()
if (_slotValue[0] === '<' && _slotValue[_slotValue.length - 1] === '>' && document.body.childElementCount === 1) {
if (_slotValue[0] === '<' && _slotValue[_slotValue.length - 1] === '>' && _document.body.childElementCount === 1) {
elem = vm.$createElement(compileToFunctions(slotValue))
} else {
const compiledResult = compileToFunctions(`<div>${slotValue}{{ }}</div>`)
Expand Down
36 changes: 36 additions & 0 deletions packages/create-instance/create-instance.js
@@ -1,6 +1,8 @@
// @flow

import Vue from 'vue'
import { addSlots } from './add-slots'
import { addScopedSlots } from './add-scoped-slots'
import addMocks from './add-mocks'
import addAttrs from './add-attrs'
import addListeners from './add-listeners'
Expand Down Expand Up @@ -57,6 +59,40 @@ export default function createInstance (
addAttrs(vm, options.attrs)
addListeners(vm, options.listeners)

if (options.scopedSlots) {
if (window.navigator.userAgent.match(/PhantomJS/i)) {
throwError('the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.')
}
const vueVersion = Number(`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`)
if (vueVersion >= 2.5) {
vm.$_vueTestUtils_scopedSlots = {}
vm.$_vueTestUtils_slotScopes = {}
const renderSlot = vm._renderProxy._t

vm._renderProxy._t = function (name, feedback, props, bindObject) {
const scopedSlotFn = vm.$_vueTestUtils_scopedSlots[name]
const slotScope = vm.$_vueTestUtils_slotScopes[name]
if (scopedSlotFn) {
props = { ...bindObject, ...props }
const proxy = {}
const helpers = ['_c', '_o', '_n', '_s', '_l', '_t', '_q', '_i', '_m', '_f', '_k', '_b', '_v', '_e', '_u', '_g']
helpers.forEach((key) => {
proxy[key] = vm._renderProxy[key]
})
proxy[slotScope] = props
return scopedSlotFn.call(proxy)
} else {
return renderSlot.call(vm._renderProxy, name, feedback, props, bindObject)
}
}

// $FlowIgnore
addScopedSlots(vm, options.scopedSlots)
} else {
throwError('the scopedSlots option is only supported in vue@2.5+.')
}
}

if (options.slots) {
addSlots(vm, options.slots)
}
Expand Down
1 change: 1 addition & 0 deletions packages/server-test-utils/types/index.d.ts
Expand Up @@ -32,6 +32,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> {
localVue?: typeof Vue
mocks?: object
slots?: Slots
scopedSlots?: Record<string, string>
stubs?: Stubs,
attrs?: object
listeners?: object
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/types/index.d.ts
Expand Up @@ -121,6 +121,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> {
localVue?: typeof Vue
mocks?: object
slots?: Slots
scopedSlots?: Record<string, string>
stubs?: Stubs,
attrs?: object
listeners?: object
Expand Down
3 changes: 3 additions & 0 deletions packages/test-utils/types/test/mount.ts
Expand Up @@ -33,6 +33,9 @@ mount(ClassComponent, {
foo: [normalOptions, functionalOptions],
bar: ClassComponent
},
scopedSlots: {
baz: `<div>Baz</div>`
},
stubs: {
foo: normalOptions,
bar: functionalOptions,
Expand Down
33 changes: 33 additions & 0 deletions test/resources/components/component-with-scoped-slots.vue
@@ -0,0 +1,33 @@
<template>
<div>
<div id="foo">
<slot name="foo"
v-for="(item, index) in foo"
:text="item.text"
:index="index">
</slot>
</div>
<div id="bar">
<slot name="bar"
v-for="(item, index) in bar"
:text="item.text"
:index="index">
</slot>
</div>
<div id="slots">
<slot></slot>
</div>
</div>
</template>

<script>
export default {
name: 'component-with-scoped-slots',
data () {
return {
foo: [{ text: 'a1' }, { text: 'a2' }, { text: 'a3' }],
bar: [{ text: 'A1' }, { text: 'A2' }, { text: 'A3' }]
}
}
}
</script>
81 changes: 81 additions & 0 deletions test/specs/mounting-options/scopedSlots.spec.js
@@ -0,0 +1,81 @@
import { describeWithShallowAndMount, vueVersion, itDoNotRunIf } from '~resources/utils'
import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue'

describeWithShallowAndMount('scopedSlots', (mountingMethod) => {
let _window

beforeEach(() => {
_window = window
})

afterEach(() => {
if (!window.navigator.userAgent.match(/Chrome/i)) {
window = _window // eslint-disable-line no-native-reassign
}
})

itDoNotRunIf(vueVersion < 2.5,
'mounts component scoped slots', () => {
const wrapper = mountingMethod(ComponentWithScopedSlots, {
slots: { default: '<span>123</span>' },
scopedSlots: {
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>',
'bar': '<p slot-scope="bar">{{bar.text}},{{bar.index}}</p>'
}
})
expect(wrapper.find('#slots').html()).to.equal('<div id="slots"><span>123</span></div>')
expect(wrapper.find('#foo').html()).to.equal('<div id="foo"><p>0,a1</p><p>1,a2</p><p>2,a3</p></div>')
expect(wrapper.find('#bar').html()).to.equal('<div id="bar"><p>A1,0</p><p>A2,1</p><p>A3,2</p></div>')
wrapper.vm.foo = [{ text: 'b1' }, { text: 'b2' }, { text: 'b3' }]
wrapper.vm.bar = [{ text: 'B1' }, { text: 'B2' }, { text: 'B3' }]
expect(wrapper.find('#foo').html()).to.equal('<div id="foo"><p>0,b1</p><p>1,b2</p><p>2,b3</p></div>')
expect(wrapper.find('#bar').html()).to.equal('<div id="bar"><p>B1,0</p><p>B2,1</p><p>B3,2</p></div>')
}
)

itDoNotRunIf(vueVersion < 2.5,
'throws exception when it is seted to a template tag at top', () => {
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
'foo': '<template></template>'
}
})
}
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,
'throws exception when vue version < 2.5', () => {
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
})
}
const message = '[vue-test-utils]: the scopedSlots option is only supported in vue@2.5+.'
expect(fn).to.throw().with.property('message', message)
}
)

itDoNotRunIf(vueVersion < 2.5,
'throws exception when using PhantomJS', () => {
if (window.navigator.userAgent.match(/Chrome/i)) {
return
}
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
})
}
const message = '[vue-test-utils]: the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.'
expect(fn).to.throw().with.property('message', message)
}
)
})
2 changes: 1 addition & 1 deletion test/specs/mounting-options/slots.spec.js
Expand Up @@ -76,7 +76,7 @@ describeWithMountingMethods('options.slots', (mountingMethod) => {
return
}
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
const message = '[vue-test-utils]: option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component'
const message = '[vue-test-utils]: the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.'
const fn = () => mountingMethod(ComponentWithSlots, { slots: { default: 'foo' }})
expect(fn).to.throw().with.property('message', message)
})
Expand Down

0 comments on commit e6a54b2

Please sign in to comment.