-
Notifications
You must be signed in to change notification settings - Fork 669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add scoped slots option #507
Changes from 18 commits
cecf8eb
e7da1f7
37c5950
3c841a1
ea2d16f
8f3239f
d4e7bdf
e6ad08d
8da1a4e
7665971
0953171
f737344
d925dcf
58399c5
4678bfb
032513c
a1f008c
cb000ea
4690a6a
73a7e5f
125c39c
303d1a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -68,6 +69,32 @@ 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). | ||
|
||
### `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 [email protected]+. | ||
|
||
* You can not use `<template>` tag as the root element in the `scopedSlots` option. | ||
|
||
* This does not support PhantomJS. | ||
Please use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer). | ||
|
||
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` | ||
|
||
- type: `{ [name: string]: Component | boolean } | Array<string>` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you rename this variable to avoid collisions with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed it. |
||
vm.$_vueTestUtils_scopedSlots[key] = compileToFunctions(template).render | ||
vm.$_vueTestUtils_slotScopes[key] = document.body.firstChild.getAttribute('slot-scope') | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' | ||
|
@@ -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 strings in 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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to set vm._renderProxy.props? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a bug. |
||
const proxy = {} | ||
const helpers = ['_c', '_o', '_n', '_s', '_l', '_t', '_q', '_i', '_m', '_f', '_k', '_b', '_v', '_e', '_u', '_g'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
helpers.forEach((key) => { | ||
proxy[key] = vm._renderProxy[key] | ||
}) | ||
proxy[slotScope] = props | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since when
scopedSlotFn
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does Vue throw an error if there are two conflicting slot scopes? If not, we should throw an error here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is impossible to set a word that starts with "_" in Vue.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant if you mounted with this option:
The second scopedSlot would overwrite the first scopedSlots in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not possible to register the same key.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤦♂️ Of course ;p |
||
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 [email protected]+.') | ||
} | ||
} | ||
|
||
if (options.slots) { | ||
addSlots(vm, options.slots) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,6 +113,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> { | |
localVue?: typeof Vue | ||
mocks?: object | ||
slots?: Slots | ||
scopedSlots?: Record<string, string> | ||
stubs?: Stubs, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be an object. Can you add a test for this in types/test? |
||
attrs?: object | ||
listeners?: object | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 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 [email protected]+.' | ||
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 strings in PhantomJS. Please use Puppeteer, or pass a component.' | ||
expect(fn).to.throw().with.property('message', message) | ||
} | ||
) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change to: