diff --git a/README.md b/README.md index d68441e8c..b7b975584 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ component | ✅ | (new!) nested in [`global`](https://vuejs.github.io/vue-test-u directives | ✅ | (new!) nested in [`global`](https://vuejs.github.io/vue-test-utils-next-docs/api/#global) stubs | ✅ attachToDocument | ❌| will rename to `attachTo`. See [here](https://github.com/vuejs/vue-test-utils/pull/1492) +attachTo | ✅ attrs | ❌ | scopedSlots | ⚰️ | scopedSlots are merged with slots in Vue 3 context | ⚰️ | different from Vue 2, may not make sense anymore. diff --git a/rollup.config.js b/rollup.config.js index dbc3e5589..2a678453e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -27,6 +27,7 @@ function createEntry(options) { 'lodash/upperFirst', 'lodash/kebabCase', 'lodash/flow', + 'lodash/isString', 'dom-event-types' ], plugins: [resolve()], diff --git a/src/mount.ts b/src/mount.ts index 0f03bcd40..c3484d51e 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -15,7 +15,7 @@ import { import { config } from './config' import { GlobalMountOptions } from './types' -import { mergeGlobalProperties } from './utils' +import { mergeGlobalProperties, isString } from './utils' import { createWrapper, VueWrapper } from './vue-wrapper' import { attachEmitListener } from './emitMixin' import { createDataMixin } from './dataMixin' @@ -36,6 +36,7 @@ interface MountingOptions { [key: string]: Slot } global?: GlobalMountOptions + attachTo?: HTMLElement | string } // Component declared with defineComponent @@ -76,10 +77,21 @@ export function mount( ): VueWrapper { const component = { ...originalComponent } - // Reset the document.body - document.getElementsByTagName('html')[0].innerHTML = '' const el = document.createElement('div') el.id = MOUNT_ELEMENT_ID + + if (options?.attachTo) { + const to = isString(options.attachTo) + ? document.querySelector(options.attachTo) + : options.attachTo + + el.appendChild(to) + } + if (el.children.length === 0) { + // Reset the document.body + document.getElementsByTagName('html')[0].innerHTML = '' + } + document.body.appendChild(el) // handle any slots passed via mounting options diff --git a/src/utils.ts b/src/utils.ts index ca7eb4106..83c969f33 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,12 +2,13 @@ import camelCase from 'lodash/camelCase' import upperFirst from 'lodash/upperFirst' import kebabCase from 'lodash/kebabCase' import flow from 'lodash/flow' +import isString from 'lodash/isString' import mergeWith from 'lodash/mergeWith' import { GlobalMountOptions } from './types' const pascalCase = flow(camelCase, upperFirst) -export { kebabCase, pascalCase } +export { kebabCase, pascalCase, isString } export function mergeGlobalProperties( configGlobal: GlobalMountOptions = {}, diff --git a/tests/mountingOptions/attachTo.spec.ts b/tests/mountingOptions/attachTo.spec.ts new file mode 100644 index 000000000..c09475ff2 --- /dev/null +++ b/tests/mountingOptions/attachTo.spec.ts @@ -0,0 +1,69 @@ +import { mount } from '../../src' + +const innerHTML = 'Hello world' +const outerHTML = `
${innerHTML}
` +const ssrHTML = `
${innerHTML}
` +const template = '
Hello world
' +const TestComponent = { template } + +describe('options.attachTo', () => { + it('attaches to a provided HTMLElement', () => { + const div = document.createElement('div') + div.id = 'root' + document.body.appendChild(div) + expect(document.getElementById('root')).not.toBeNull() + expect(document.getElementById('attach-to')).toBeNull() + const wrapper = mount(TestComponent, { + attachTo: div + }) + + const root = document.getElementById('root') + const rendered = document.getElementById('attach-to') + expect(wrapper.vm.$el.parentNode).not.toBeNull() + expect(root).toBeNull() + expect(rendered).not.toBeNull() + expect(rendered.outerHTML).toBe(outerHTML) + wrapper.unmount() + expect(document.getElementById('attach-to')).toBeNull() + }) + it('attaches to a provided CSS selector string', () => { + const div = document.createElement('div') + div.id = 'root' + document.body.appendChild(div) + expect(document.getElementById('root')).not.toBeNull() + expect(document.getElementById('attach-to')).toBeNull() + const wrapper = mount(TestComponent, { + attachTo: '#root' + }) + + const root = document.getElementById('root') + const rendered = document.getElementById('attach-to') + expect(wrapper.vm.$el.parentNode).not.toBeNull() + expect(root).toBeNull() + expect(rendered).not.toBeNull() + expect(rendered.outerHTML).toBe(outerHTML) + wrapper.unmount() + expect(document.getElementById('attach-to')).toBeNull() + }) + + it('correctly hydrates markup', () => { + expect(document.getElementById('attach-to')).toBeNull() + + 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).toBe(ssrHTML) + const wrapper = mount(TestComponent, { + attachTo: '#attach-to' + }) + + const rendered = document.getElementById('attach-to') + expect(wrapper.vm.$el.parentNode).not.toBeNull() + expect(rendered).not.toBeNull() + expect(rendered.outerHTML).toBe(outerHTML) + wrapper.unmount() + expect(document.getElementById('attach-to')).toBeNull() + }) +})