Skip to content

Commit e6a54b2

Browse files
38elementseddyerburgh
authored andcommittedApr 14, 2018
feat: add scoped slots option (#507)
1 parent fa45baf commit e6a54b2

File tree

14 files changed

+208
-5
lines changed

14 files changed

+208
-5
lines changed
 

‎docs/en/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Vue Test Utils is the official unit testing utility library for Vue.js.
2323
* [Mounting Options](api/options.md)
2424
- [context](api/options.md#context)
2525
- [slots](api/options.md#slots)
26+
- [scopedSlots](api/options.md#scopedslots)
2627
- [stubs](api/options.md#stubs)
2728
- [mocks](api/options.md#mocks)
2829
- [localVue](api/options.md#localvue)

‎docs/en/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* [Mounting Options](api/options.md)
2020
- [context](api/options.md#context)
2121
- [slots](api/options.md#slots)
22+
- [scopedSlots](api/options.md#scopedslots)
2223
- [stubs](api/options.md#stubs)
2324
- [mocks](api/options.md#mocks)
2425
- [localVue](api/options.md#localvue)

‎docs/en/api/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [Mounting Options](./options.md)
88
- [context](./options.md#context)
99
- [slots](./options.md#slots)
10+
- [scopedSlots](./options.md#scopedslots)
1011
- [stubs](./options.md#stubs)
1112
- [mocks](./options.md#mocks)
1213
- [localVue](./options.md#localvue)

‎docs/en/api/options.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Options for `mount` and `shallow`. The options object can contain both Vue Test
66

77
- [`context`](#context)
88
- [`slots`](#slots)
9+
- [`scopedSlots`](#scopedslots)
910
- [`stubs`](#stubs)
1011
- [`mocks`](#mocks)
1112
- [`localVue`](#localvue)
@@ -66,7 +67,33 @@ You can pass text to `slots`.
6667
There is a limitation to this.
6768

6869
This does not support PhantomJS.
69-
Please use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer).
70+
You can use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer) as an alternative.
71+
72+
### `scopedSlots`
73+
74+
- type: `{ [name: string]: string }`
75+
76+
Provide an object of scoped slots contents to the component. The key corresponds to the slot name. The value can be a template string.
77+
78+
There are three limitations.
79+
80+
* This option is only supported in vue@2.5+.
81+
82+
* You can not use `<template>` tag as the root element in the `scopedSlots` option.
83+
84+
* This does not support PhantomJS.
85+
You can use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer) as an alternative.
86+
87+
Example:
88+
89+
```js
90+
const wrapper = shallow(Component, {
91+
scopedSlots: {
92+
foo: '<p slot-scope="props">{{props.index}},{{props.text}}</p>'
93+
}
94+
})
95+
expect(wrapper.find('#fooWrapper').html()).toBe('<div id="fooWrapper"><p>0,text1</p><p>1,text2</p><p>2,text3</p></div>')
96+
```
7097

7198
### `stubs`
7299

‎flow/options.flow.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ declare type Options = { // eslint-disable-line no-undef
22
attachToDocument?: boolean,
33
mocks?: Object,
44
slots?: Object,
5+
scopedSlots?: Object,
56
localVue?: Component,
67
provide?: Object,
78
stubs?: Object,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @flow
2+
3+
import { compileToFunctions } from 'vue-template-compiler'
4+
import { throwError } from 'shared/util'
5+
6+
export function addScopedSlots (vm: Component, scopedSlots: Object): void {
7+
Object.keys(scopedSlots).forEach((key) => {
8+
const template = scopedSlots[key].trim()
9+
if (template.substr(0, 9) === '<template') {
10+
throwError('the scopedSlots option does not support a template tag as the root element.')
11+
}
12+
const domParser = new window.DOMParser()
13+
const _document = domParser.parseFromString(template, 'text/html')
14+
vm.$_vueTestUtils_scopedSlots[key] = compileToFunctions(template).render
15+
vm.$_vueTestUtils_slotScopes[key] = _document.body.firstChild.getAttribute('slot-scope')
16+
})
17+
}

‎packages/create-instance/add-slots.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ function addSlotToVm (vm: Component, slotName: string, slotValue: Component | st
1111
throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined')
1212
}
1313
if (window.navigator.userAgent.match(/PhantomJS/i)) {
14-
throwError('option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component')
14+
throwError('the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.')
1515
}
1616
const domParser = new window.DOMParser()
17-
const document = domParser.parseFromString(slotValue, 'text/html')
17+
const _document = domParser.parseFromString(slotValue, 'text/html')
1818
const _slotValue = slotValue.trim()
19-
if (_slotValue[0] === '<' && _slotValue[_slotValue.length - 1] === '>' && document.body.childElementCount === 1) {
19+
if (_slotValue[0] === '<' && _slotValue[_slotValue.length - 1] === '>' && _document.body.childElementCount === 1) {
2020
elem = vm.$createElement(compileToFunctions(slotValue))
2121
} else {
2222
const compiledResult = compileToFunctions(`<div>${slotValue}{{ }}</div>`)

‎packages/create-instance/create-instance.js

+36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// @flow
22

3+
import Vue from 'vue'
34
import { addSlots } from './add-slots'
5+
import { addScopedSlots } from './add-scoped-slots'
46
import addMocks from './add-mocks'
57
import addAttrs from './add-attrs'
68
import addListeners from './add-listeners'
@@ -57,6 +59,40 @@ export default function createInstance (
5759
addAttrs(vm, options.attrs)
5860
addListeners(vm, options.listeners)
5961

62+
if (options.scopedSlots) {
63+
if (window.navigator.userAgent.match(/PhantomJS/i)) {
64+
throwError('the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.')
65+
}
66+
const vueVersion = Number(`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`)
67+
if (vueVersion >= 2.5) {
68+
vm.$_vueTestUtils_scopedSlots = {}
69+
vm.$_vueTestUtils_slotScopes = {}
70+
const renderSlot = vm._renderProxy._t
71+
72+
vm._renderProxy._t = function (name, feedback, props, bindObject) {
73+
const scopedSlotFn = vm.$_vueTestUtils_scopedSlots[name]
74+
const slotScope = vm.$_vueTestUtils_slotScopes[name]
75+
if (scopedSlotFn) {
76+
props = { ...bindObject, ...props }
77+
const proxy = {}
78+
const helpers = ['_c', '_o', '_n', '_s', '_l', '_t', '_q', '_i', '_m', '_f', '_k', '_b', '_v', '_e', '_u', '_g']
79+
helpers.forEach((key) => {
80+
proxy[key] = vm._renderProxy[key]
81+
})
82+
proxy[slotScope] = props
83+
return scopedSlotFn.call(proxy)
84+
} else {
85+
return renderSlot.call(vm._renderProxy, name, feedback, props, bindObject)
86+
}
87+
}
88+
89+
// $FlowIgnore
90+
addScopedSlots(vm, options.scopedSlots)
91+
} else {
92+
throwError('the scopedSlots option is only supported in vue@2.5+.')
93+
}
94+
}
95+
6096
if (options.slots) {
6197
addSlots(vm, options.slots)
6298
}

‎packages/server-test-utils/types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> {
3232
localVue?: typeof Vue
3333
mocks?: object
3434
slots?: Slots
35+
scopedSlots?: Record<string, string>
3536
stubs?: Stubs,
3637
attrs?: object
3738
listeners?: object

‎packages/test-utils/types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> {
121121
localVue?: typeof Vue
122122
mocks?: object
123123
slots?: Slots
124+
scopedSlots?: Record<string, string>
124125
stubs?: Stubs,
125126
attrs?: object
126127
listeners?: object

‎packages/test-utils/types/test/mount.ts

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ mount(ClassComponent, {
3333
foo: [normalOptions, functionalOptions],
3434
bar: ClassComponent
3535
},
36+
scopedSlots: {
37+
baz: `<div>Baz</div>`
38+
},
3639
stubs: {
3740
foo: normalOptions,
3841
bar: functionalOptions,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div>
3+
<div id="foo">
4+
<slot name="foo"
5+
v-for="(item, index) in foo"
6+
:text="item.text"
7+
:index="index">
8+
</slot>
9+
</div>
10+
<div id="bar">
11+
<slot name="bar"
12+
v-for="(item, index) in bar"
13+
:text="item.text"
14+
:index="index">
15+
</slot>
16+
</div>
17+
<div id="slots">
18+
<slot></slot>
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script>
24+
export default {
25+
name: 'component-with-scoped-slots',
26+
data () {
27+
return {
28+
foo: [{ text: 'a1' }, { text: 'a2' }, { text: 'a3' }],
29+
bar: [{ text: 'A1' }, { text: 'A2' }, { text: 'A3' }]
30+
}
31+
}
32+
}
33+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describeWithShallowAndMount, vueVersion, itDoNotRunIf } from '~resources/utils'
2+
import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue'
3+
4+
describeWithShallowAndMount('scopedSlots', (mountingMethod) => {
5+
let _window
6+
7+
beforeEach(() => {
8+
_window = window
9+
})
10+
11+
afterEach(() => {
12+
if (!window.navigator.userAgent.match(/Chrome/i)) {
13+
window = _window // eslint-disable-line no-native-reassign
14+
}
15+
})
16+
17+
itDoNotRunIf(vueVersion < 2.5,
18+
'mounts component scoped slots', () => {
19+
const wrapper = mountingMethod(ComponentWithScopedSlots, {
20+
slots: { default: '<span>123</span>' },
21+
scopedSlots: {
22+
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>',
23+
'bar': '<p slot-scope="bar">{{bar.text}},{{bar.index}}</p>'
24+
}
25+
})
26+
expect(wrapper.find('#slots').html()).to.equal('<div id="slots"><span>123</span></div>')
27+
expect(wrapper.find('#foo').html()).to.equal('<div id="foo"><p>0,a1</p><p>1,a2</p><p>2,a3</p></div>')
28+
expect(wrapper.find('#bar').html()).to.equal('<div id="bar"><p>A1,0</p><p>A2,1</p><p>A3,2</p></div>')
29+
wrapper.vm.foo = [{ text: 'b1' }, { text: 'b2' }, { text: 'b3' }]
30+
wrapper.vm.bar = [{ text: 'B1' }, { text: 'B2' }, { text: 'B3' }]
31+
expect(wrapper.find('#foo').html()).to.equal('<div id="foo"><p>0,b1</p><p>1,b2</p><p>2,b3</p></div>')
32+
expect(wrapper.find('#bar').html()).to.equal('<div id="bar"><p>B1,0</p><p>B2,1</p><p>B3,2</p></div>')
33+
}
34+
)
35+
36+
itDoNotRunIf(vueVersion < 2.5,
37+
'throws exception when it is seted to a template tag at top', () => {
38+
const fn = () => {
39+
mountingMethod(ComponentWithScopedSlots, {
40+
scopedSlots: {
41+
'foo': '<template></template>'
42+
}
43+
})
44+
}
45+
const message = '[vue-test-utils]: the scopedSlots option does not support a template tag as the root element.'
46+
expect(fn).to.throw().with.property('message', message)
47+
}
48+
)
49+
50+
itDoNotRunIf(vueVersion >= 2.5,
51+
'throws exception when vue version < 2.5', () => {
52+
const fn = () => {
53+
mountingMethod(ComponentWithScopedSlots, {
54+
scopedSlots: {
55+
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
56+
}
57+
})
58+
}
59+
const message = '[vue-test-utils]: the scopedSlots option is only supported in vue@2.5+.'
60+
expect(fn).to.throw().with.property('message', message)
61+
}
62+
)
63+
64+
itDoNotRunIf(vueVersion < 2.5,
65+
'throws exception when using PhantomJS', () => {
66+
if (window.navigator.userAgent.match(/Chrome/i)) {
67+
return
68+
}
69+
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
70+
const fn = () => {
71+
mountingMethod(ComponentWithScopedSlots, {
72+
scopedSlots: {
73+
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
74+
}
75+
})
76+
}
77+
const message = '[vue-test-utils]: the scopedSlots option does not support PhantomJS. Please use Puppeteer, or pass a component.'
78+
expect(fn).to.throw().with.property('message', message)
79+
}
80+
)
81+
})

‎test/specs/mounting-options/slots.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describeWithMountingMethods('options.slots', (mountingMethod) => {
7676
return
7777
}
7878
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
79-
const message = '[vue-test-utils]: option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component'
79+
const message = '[vue-test-utils]: the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.'
8080
const fn = () => mountingMethod(ComponentWithSlots, { slots: { default: 'foo' }})
8181
expect(fn).to.throw().with.property('message', message)
8282
})

0 commit comments

Comments
 (0)
Please sign in to comment.