Skip to content

Commit

Permalink
feat: add support of arbitrary mounting point via attachTo option
Browse files Browse the repository at this point in the history
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 #1492
  • Loading branch information
jnields committed Mar 30, 2020
1 parent 0c4a811 commit dbb715a
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 4 deletions.
2 changes: 2 additions & 0 deletions flow/options.flow.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
declare type Options = {
// eslint-disable-line no-undef
attachToDocument?: boolean,
attachTo?: HTMLElement | string,
propsData?: Object,
mocks?: Object,
methods?: { [key: string]: Function },
Expand All @@ -17,6 +18,7 @@ declare type Options = {
}

declare type NormalizedOptions = {
attachTo?: HTMLElement | string,
attachToDocument?: boolean,
propsData?: Object,
mocks: Object,
Expand Down
20 changes: 18 additions & 2 deletions packages/shared/validate-options.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
isPlainObject,
isFunctionalComponent,
isConstructor
isConstructor,
isDomSelector,
isHTMLElement
} from './validators'
import { VUE_VERSION } from './consts'
import { compileTemplateForSlots } from './compile-template'
import { throwError } from './util'
import { throwError, warn } from './util'
import { validateSlots } from './validate-slots'

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

export function validateOptions(options, component) {
if (
options.attachTo &&
!isHTMLElement(options.attachTo) &&
!isDomSelector(options.attachTo)
) {
throwError(
`options.attachTo should be a valid HTMLElement or CSS selector string`
)
}
if ('attachToDocument' in options) {
warn(
`options.attachToDocument is deprecated in favor of options.attachTo and will be removed in a future release`
)
}
if (options.parentComponent && !isPlainObject(options.parentComponent)) {
throwError(
`options.parentComponent should be a valid Vue component options object`
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ export function isPlainObject(c: any): boolean {
return Object.prototype.toString.call(c) === '[object Object]'
}

export function isHTMLElement(c: any): boolean {
if (typeof HTMLElement === 'undefined') {
return false
}
// eslint-disable-next-line no-undef
return c instanceof HTMLElement
}

export function isRequiredComponent(name: string): boolean {
return (
name === 'KeepAlive' || name === 'Transition' || name === 'TransitionGroup'
Expand Down
5 changes: 3 additions & 2 deletions packages/test-utils/src/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ export default function mount(component, options = {}) {

const parentVm = createInstance(component, mergedOptions, _Vue)

const el = options.attachToDocument ? createElement() : undefined
const el =
options.attachTo || (options.attachToDocument ? createElement() : undefined)
const vm = parentVm.$mount(el)

component._Ctor = {}

throwIfInstancesThrew(vm)

const wrapperOptions = {
attachedToDocument: !!mergedOptions.attachToDocument
attachedToDocument: !!el
}

const root = parentVm.$options._isFunctionalContainer
Expand Down
2 changes: 2 additions & 0 deletions test/specs/mount.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => {
expect(wrapper.vm.$options.context).to.equal(undefined)
expect(wrapper.vm.$options.attrs).to.equal(undefined)
expect(wrapper.vm.$options.listeners).to.equal(undefined)
wrapper.destroy()
})

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

it('overwrites the component options with the instance options', () => {
Expand Down
62 changes: 62 additions & 0 deletions test/specs/mounting-options/attachTo.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describeWithShallowAndMount } from '~resources/utils'

const innerHTML = '<input><span>Hello world</span>'
const outerHTML = `<div id="attach-to">${innerHTML}</div>`
const ssrHTML = `<div id="attach-to" data-server-rendered="true">${innerHTML}</div>`
const template = '<div id="attach-to"><input /><span>Hello world</span></div>'
const TestComponent = { template }

describeWithShallowAndMount('options.attachTo', mountingMethod => {
it('attaches to a provided HTMLElement', () => {
const div = document.createElement('div')
div.id = 'root'
document.body.appendChild(div)
expect(document.getElementById('root')).to.not.be.null
expect(document.getElementById('attach-to')).to.be.null
const wrapper = mountingMethod(TestComponent, {
attachTo: div
})
expect(document.getElementById('root')).to.be.null
expect(document.getElementById('attach-to')).to.not.be.null
expect(document.getElementById('attach-to').outerHTML).to.equal(outerHTML)
expect(wrapper.options.attachedToDocument).to.equal(true)
wrapper.destroy()
expect(document.getElementById('attach-to')).to.be.null
})
it('attaches to a provided CSS selector string', () => {
const div = document.createElement('div')
div.id = 'root'
document.body.appendChild(div)
expect(document.getElementById('root')).to.not.be.null
expect(document.getElementById('attach-to')).to.be.null
const wrapper = mountingMethod(TestComponent, {
attachTo: '#root'
})
expect(document.getElementById('root')).to.be.null
expect(document.getElementById('attach-to')).to.not.be.null
expect(document.getElementById('attach-to').outerHTML).to.equal(outerHTML)
expect(wrapper.options.attachedToDocument).to.equal(true)
wrapper.destroy()
expect(document.getElementById('attach-to')).to.be.null
})

it('correctly hydrates markup', () => {
expect(document.getElementById('attach-to')).to.be.null

const div = document.createElement('div')
div.id = 'attach-to'
div.setAttribute('data-server-rendered', 'true')
div.innerHTML = innerHTML
document.body.appendChild(div)
expect(div.outerHTML).to.equal(ssrHTML)
const wrapper = mountingMethod(TestComponent, {
attachTo: '#attach-to'
})
const rendered = document.getElementById('attach-to')
expect(rendered).to.not.be.null
expect(rendered.outerHTML).to.equal(outerHTML)
expect(wrapper.options.attachedToDocument).to.equal(true)
wrapper.destroy()
expect(document.getElementById('attach-to')).to.be.null
})
})

0 comments on commit dbb715a

Please sign in to comment.