Skip to content
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

Improve passing text to slots #274

Merged
merged 14 commits into from
Dec 19, 2017
18 changes: 3 additions & 15 deletions docs/en/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,10 @@ expect(wrapper.find('div')).toBe(true)
#### Passing text

You can pass text to `slots`.
There are two limitations to this.
There is a limitation to this.

This works with Vue 2.2+.

This works for the text below.

```js
const wrapper = mount(ComponentWithSlots, { slots: { default: 'foobar' }})
```

This does not work for the text below.

```js
const wrapper1 = mount(ComponentWithSlots, { slots: { default: 'foo<span>bar</span>' }})
const wrapper2 = mount(FooComponent, { slots: { default: 'foo {{ bar }}' }})
```
This does not support PhantomJS.
Please use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer).

### `stubs`

Expand Down
34 changes: 23 additions & 11 deletions src/lib/add-slots.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @flow

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

Expand All @@ -10,27 +9,40 @@ function isValidSlot (slot: any): boolean {

function addSlotToVm (vm: Component, slotName: string, slotValue: Component | string | Array<Component> | Array<string>): void {
let elem
const vueVersion = Number(`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`)
if (typeof slotValue === 'string') {
if (!compileToFunctions) {
throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined')
}
if (slotValue.trim()[0] === '<') {
if (window.navigator.userAgent.match(/PhantomJS/i)) {
throwError('option.slots 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 _slotValue = slotValue.trim()
if (_slotValue[0] === '<' && _slotValue[_slotValue.length - 1] === '>' && document.body.childElementCount === 1) {
elem = vm.$createElement(compileToFunctions(slotValue))
} else {
if (vueVersion >= 2.2) {
elem = vm._v(slotValue)
} else {
throwError('vue-test-utils support for passing text to slots at vue@2.2+')
}
const compiledResult = compileToFunctions(`<div>${slotValue}{{ }}</div>`)
const _staticRenderFns = vm._renderProxy.$options.staticRenderFns
vm._renderProxy.$options.staticRenderFns = compiledResult.staticRenderFns
elem = compiledResult.render.call(vm._renderProxy, vm.$createElement).children
vm._renderProxy.$options.staticRenderFns = _staticRenderFns
}
} else {
elem = vm.$createElement(slotValue)
}
if (Array.isArray(vm.$slots[slotName])) {
vm.$slots[slotName].push(elem)
if (Array.isArray(elem)) {
if (Array.isArray(vm.$slots[slotName])) {
vm.$slots[slotName] = [...vm.$slots[slotName], ...elem]
} else {
vm.$slots[slotName] = [...elem]
}
} else {
vm.$slots[slotName] = [elem]
if (Array.isArray(vm.$slots[slotName])) {
vm.$slots[slotName].push(elem)
} else {
vm.$slots[slotName] = [elem]
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/lib/create-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import createLocalVue from '../create-local-vue'
import extractOptions from '../options/extract-options'
import deleteMountingOptions from '../options/delete-mounting-options'
import createFunctionalComponent from './create-functional-component'
import cloneDeep from 'lodash/cloneDeep'

export default function createConstructor (
component: Component,
Expand Down Expand Up @@ -58,6 +59,9 @@ export default function createConstructor (
addAttrs(vm, mountingOptions.attrs)
addListeners(vm, mountingOptions.listeners)

vm.$_mountingOptionsSlots = mountingOptions.slots
vm.$_originalSlots = cloneDeep(vm.$slots)

if (mountingOptions.slots) {
addSlots(vm, mountingOptions.slots)
}
Expand Down
12 changes: 11 additions & 1 deletion src/wrappers/vue-wrapper.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// @flow

import Wrapper from './wrapper'
import addSlots from '../lib/add-slots'
import cloneDeep from 'lodash/cloneDeep'

function update () {
this._update(this._render())
// the only component made by mount()
if (this.$_originalSlots) {
this.$slots = cloneDeep(this.$_originalSlots)
}
if (this.$_mountingOptionsSlots) {
addSlots(this, this.$_mountingOptionsSlots)
}
const vnodes = this._render()
this._update(vnodes)
this.$children.forEach(child => update.call(child))
}

Expand Down
14 changes: 12 additions & 2 deletions test/resources/components/component-with-slots.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="container">
<div class="container" @keydown="change">
<header>
<slot name="header"></slot>
</header>
Expand All @@ -14,6 +14,16 @@

<script>
export default {
name: 'component-with-slots'
name: 'component-with-slots',
data () {
return {
'foo': 'bar'
}
},
methods: {
change () {
this.foo = 'BAR'
}
}
}
</script>
57 changes: 40 additions & 17 deletions test/unit/specs/mount/options/slots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ import { compileToFunctions } from 'vue-template-compiler'
import { mount } from '~vue-test-utils'
import Component from '~resources/components/component.vue'
import ComponentWithSlots from '~resources/components/component-with-slots.vue'
import { vueVersion } from '~resources/test-utils'

describe('mount.slots', () => {
let _window

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

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

it('mounts component with default slot if passed component in slot object', () => {
const wrapper = mount(ComponentWithSlots, { slots: { default: Component }})
expect(wrapper.contains(Component)).to.equal(true)
Expand All @@ -26,15 +37,33 @@ describe('mount.slots', () => {
expect(wrapper.contains('span')).to.equal(true)
})

it('mounts component with default slot if passed string in slot object', () => {
if (vueVersion >= 2.2) {
const wrapper = mount(ComponentWithSlots, { slots: { default: 'foo' }})
expect(wrapper.find('main').text()).to.equal('foo')
} else {
const message = '[vue-test-utils]: vue-test-utils support for passing text to slots at vue@2.2+'
const fn = () => mount(ComponentWithSlots, { slots: { default: 'foo' }})
expect(fn).to.throw().with.property('message', message)
it('throws error if the UserAgent is PhantomJS when passed string is in slot object', () => {
if (window.navigator.userAgent.match(/Chrome/i)) {
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 fn = () => mount(ComponentWithSlots, { slots: { default: 'foo' }})
expect(fn).to.throw().with.property('message', message)
})

it('mounts component with default slot if passed string in slot object', () => {
const wrapper1 = mount(ComponentWithSlots, { slots: { default: 'foo<span>123</span>{{ foo }}' }})
expect(wrapper1.find('main').html()).to.equal('<main>foo<span>123</span>bar</main>')
const wrapper2 = mount(ComponentWithSlots, { slots: { default: '<p>1</p>{{ foo }}2' }})
expect(wrapper2.find('main').html()).to.equal('<main><p>1</p>bar2</main>')
const wrapper3 = mount(ComponentWithSlots, { slots: { default: '<p>1</p>{{ foo }}<p>2</p>' }})
expect(wrapper3.find('main').html()).to.equal('<main><p>1</p>bar<p>2</p></main>')
const wrapper4 = mount(ComponentWithSlots, { slots: { default: '123' }})
expect(wrapper4.find('main').html()).to.equal('<main>123</main>')
const wrapper5 = mount(ComponentWithSlots, { slots: { default: '1{{ foo }}2' }})
expect(wrapper5.find('main').html()).to.equal('<main>1bar2</main>')
wrapper5.trigger('keydown')
expect(wrapper5.find('main').html()).to.equal('<main>1BAR2</main>')
const wrapper6 = mount(ComponentWithSlots, { slots: { default: '<p>1</p><p>2</p>' }})
expect(wrapper6.find('main').html()).to.equal('<main><p>1</p><p>2</p></main>')
const wrapper7 = mount(ComponentWithSlots, { slots: { default: '1<p>2</p>3' }})
expect(wrapper7.find('main').html()).to.equal('<main>1<p>2</p>3</main>')
})

it('throws error if passed string in default slot object and vue-template-compiler is undefined', () => {
Expand All @@ -59,14 +88,8 @@ describe('mount.slots', () => {
})

it('mounts component with default slot if passed string in slot text array object', () => {
if (vueVersion >= 2.2) {
const wrapper = mount(ComponentWithSlots, { slots: { default: ['foo', 'bar'] }})
expect(wrapper.find('main').text()).to.equal('foobar')
} else {
const message = '[vue-test-utils]: vue-test-utils support for passing text to slots at vue@2.2+'
const fn = () => mount(ComponentWithSlots, { slots: { default: ['foo', 'bar'] }})
expect(fn).to.throw().with.property('message', message)
}
const wrapper = mount(ComponentWithSlots, { slots: { default: ['{{ foo }}<span>1</span>', 'bar'] }})
expect(wrapper.find('main').html()).to.equal('<main>bar<span>1</span>bar</main>')
})

it('throws error if passed string in default slot array vue-template-compiler is undefined', () => {
Expand Down