Skip to content

Commit dbb715a

Browse files
committed
feat: add support of arbitrary mounting point via attachTo option
This allows for users to specify where in the document their component should attach, either through a CSS selector string or a provided HTMLElement. This option is passed through directly to the vm.$mount method that is called as part of mount.js. This enables testing of SSR code with Vue test utils as well as rendering of applications via Vue test utiles in contexts that aren't 100% Vue fixes vuejs#1492
1 parent 0c4a811 commit dbb715a

File tree

6 files changed

+95
-4
lines changed

6 files changed

+95
-4
lines changed

Diff for: flow/options.flow.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
declare type Options = {
22
// eslint-disable-line no-undef
33
attachToDocument?: boolean,
4+
attachTo?: HTMLElement | string,
45
propsData?: Object,
56
mocks?: Object,
67
methods?: { [key: string]: Function },
@@ -17,6 +18,7 @@ declare type Options = {
1718
}
1819

1920
declare type NormalizedOptions = {
21+
attachTo?: HTMLElement | string,
2022
attachToDocument?: boolean,
2123
propsData?: Object,
2224
mocks: Object,

Diff for: packages/shared/validate-options.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {
22
isPlainObject,
33
isFunctionalComponent,
4-
isConstructor
4+
isConstructor,
5+
isDomSelector,
6+
isHTMLElement
57
} from './validators'
68
import { VUE_VERSION } from './consts'
79
import { compileTemplateForSlots } from './compile-template'
8-
import { throwError } from './util'
10+
import { throwError, warn } from './util'
911
import { validateSlots } from './validate-slots'
1012

1113
function vueExtendUnsupportedOption(option) {
@@ -22,6 +24,20 @@ function vueExtendUnsupportedOption(option) {
2224
const UNSUPPORTED_VERSION_OPTIONS = ['mocks', 'stubs', 'localVue']
2325

2426
export function validateOptions(options, component) {
27+
if (
28+
options.attachTo &&
29+
!isHTMLElement(options.attachTo) &&
30+
!isDomSelector(options.attachTo)
31+
) {
32+
throwError(
33+
`options.attachTo should be a valid HTMLElement or CSS selector string`
34+
)
35+
}
36+
if ('attachToDocument' in options) {
37+
warn(
38+
`options.attachToDocument is deprecated in favor of options.attachTo and will be removed in a future release`
39+
)
40+
}
2541
if (options.parentComponent && !isPlainObject(options.parentComponent)) {
2642
throwError(
2743
`options.parentComponent should be a valid Vue component options object`

Diff for: packages/shared/validators.js

+8
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ export function isPlainObject(c: any): boolean {
112112
return Object.prototype.toString.call(c) === '[object Object]'
113113
}
114114

115+
export function isHTMLElement(c: any): boolean {
116+
if (typeof HTMLElement === 'undefined') {
117+
return false
118+
}
119+
// eslint-disable-next-line no-undef
120+
return c instanceof HTMLElement
121+
}
122+
115123
export function isRequiredComponent(name: string): boolean {
116124
return (
117125
name === 'KeepAlive' || name === 'Transition' || name === 'TransitionGroup'

Diff for: packages/test-utils/src/mount.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ export default function mount(component, options = {}) {
2828

2929
const parentVm = createInstance(component, mergedOptions, _Vue)
3030

31-
const el = options.attachToDocument ? createElement() : undefined
31+
const el =
32+
options.attachTo || (options.attachToDocument ? createElement() : undefined)
3233
const vm = parentVm.$mount(el)
3334

3435
component._Ctor = {}
3536

3637
throwIfInstancesThrew(vm)
3738

3839
const wrapperOptions = {
39-
attachedToDocument: !!mergedOptions.attachToDocument
40+
attachedToDocument: !!el
4041
}
4142

4243
const root = parentVm.$options._isFunctionalContainer

Diff for: test/specs/mount.spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => {
293293
expect(wrapper.vm.$options.context).to.equal(undefined)
294294
expect(wrapper.vm.$options.attrs).to.equal(undefined)
295295
expect(wrapper.vm.$options.listeners).to.equal(undefined)
296+
wrapper.destroy()
296297
})
297298

298299
itDoNotRunIf(vueVersion < 2.3, 'injects store correctly', () => {
@@ -366,6 +367,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => {
366367
' <p class="prop-2"></p>\n' +
367368
'</div>'
368369
)
370+
wrapper.destroy()
369371
})
370372

371373
it('overwrites the component options with the instance options', () => {

Diff for: test/specs/mounting-options/attachTo.spec.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describeWithShallowAndMount } from '~resources/utils'
2+
3+
const innerHTML = '<input><span>Hello world</span>'
4+
const outerHTML = `<div id="attach-to">${innerHTML}</div>`
5+
const ssrHTML = `<div id="attach-to" data-server-rendered="true">${innerHTML}</div>`
6+
const template = '<div id="attach-to"><input /><span>Hello world</span></div>'
7+
const TestComponent = { template }
8+
9+
describeWithShallowAndMount('options.attachTo', mountingMethod => {
10+
it('attaches to a provided HTMLElement', () => {
11+
const div = document.createElement('div')
12+
div.id = 'root'
13+
document.body.appendChild(div)
14+
expect(document.getElementById('root')).to.not.be.null
15+
expect(document.getElementById('attach-to')).to.be.null
16+
const wrapper = mountingMethod(TestComponent, {
17+
attachTo: div
18+
})
19+
expect(document.getElementById('root')).to.be.null
20+
expect(document.getElementById('attach-to')).to.not.be.null
21+
expect(document.getElementById('attach-to').outerHTML).to.equal(outerHTML)
22+
expect(wrapper.options.attachedToDocument).to.equal(true)
23+
wrapper.destroy()
24+
expect(document.getElementById('attach-to')).to.be.null
25+
})
26+
it('attaches to a provided CSS selector string', () => {
27+
const div = document.createElement('div')
28+
div.id = 'root'
29+
document.body.appendChild(div)
30+
expect(document.getElementById('root')).to.not.be.null
31+
expect(document.getElementById('attach-to')).to.be.null
32+
const wrapper = mountingMethod(TestComponent, {
33+
attachTo: '#root'
34+
})
35+
expect(document.getElementById('root')).to.be.null
36+
expect(document.getElementById('attach-to')).to.not.be.null
37+
expect(document.getElementById('attach-to').outerHTML).to.equal(outerHTML)
38+
expect(wrapper.options.attachedToDocument).to.equal(true)
39+
wrapper.destroy()
40+
expect(document.getElementById('attach-to')).to.be.null
41+
})
42+
43+
it('correctly hydrates markup', () => {
44+
expect(document.getElementById('attach-to')).to.be.null
45+
46+
const div = document.createElement('div')
47+
div.id = 'attach-to'
48+
div.setAttribute('data-server-rendered', 'true')
49+
div.innerHTML = innerHTML
50+
document.body.appendChild(div)
51+
expect(div.outerHTML).to.equal(ssrHTML)
52+
const wrapper = mountingMethod(TestComponent, {
53+
attachTo: '#attach-to'
54+
})
55+
const rendered = document.getElementById('attach-to')
56+
expect(rendered).to.not.be.null
57+
expect(rendered.outerHTML).to.equal(outerHTML)
58+
expect(wrapper.options.attachedToDocument).to.equal(true)
59+
wrapper.destroy()
60+
expect(document.getElementById('attach-to')).to.be.null
61+
})
62+
})

0 commit comments

Comments
 (0)