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 PhantomJS. Please use Puppeteer, or pass a component')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

option.slots does not support strings PhantomJS. Please use Puppeteer, or pass a component

to

option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it.

}
const domParser = new window.DOMParser()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOMParser isn't supported in phantomJs, so we need an alternative that will work in phantom

Copy link
Contributor Author

@38elements 38elements Dec 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for reviewing.
Is there a choice not to support PhantomJS ?
IMHO, I think it is better to use Puppeteer.
It is better to introduce Puppeteer at document.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of users still use phantom. We could throw an error, or a warning that phantom doesn't support markup in slots, and handle it gracefully

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for reply.
I will look for other ways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change the error message to—option.slots does not support strings PhantomJS. Please use Puppeteer, or pass a component

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry.
I did not notice your comment.
I fixed the error message.

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 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