diff --git a/src/mount.ts b/src/mount.ts index adf0273c1..1e63f49cd 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -3,8 +3,10 @@ import { createApp, VNode, defineComponent, - Plugin, - ComponentOptions + VNodeNormalizedChildren, + VNodeProps, + ComponentOptions, + Plugin } from 'vue' import { VueWrapper, createWrapper } from './vue-wrapper' @@ -41,15 +43,12 @@ export function mount

( document.body.appendChild(el) // handle any slots passed via mounting options - const slots = + const slots: VNodeNormalizedChildren = options?.slots && - Object.entries(options.slots).reduce VNode | string>>( - (acc, [name, fn]) => { - acc[name] = () => fn - return acc - }, - {} - ) + Object.entries(options.slots).reduce((acc, [name, fn]) => { + acc[name] = () => fn + return acc + }, {}) // override component data with mounting options data if (options?.data) { const dataMixin = createDataMixin(options.data()) @@ -57,10 +56,10 @@ export function mount

( } // create the wrapper component - const Parent = (props?: P) => + const Parent = (props?: VNodeProps) => defineComponent({ render() { - return h(component, props, slots) + return h(component, { ...props, ref: 'VTU_COMPONENT' }, slots) } }) @@ -90,7 +89,7 @@ export function mount

( vm.mixin(emitMixin) // mount the app! - const app = vm.mount('#app') + const app = vm.mount(el) return createWrapper(app, events) } diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 6855b86e5..3ba359f3a 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,24 +1,41 @@ import { ComponentPublicInstance } from 'vue' +import { ShapeFlags } from '@vue/shared' import { DOMWrapper } from './dom-wrapper' import { WrapperAPI } from './types' import { ErrorWrapper } from './error-wrapper' export class VueWrapper implements WrapperAPI { - vm: ComponentPublicInstance + rootVM: ComponentPublicInstance + componentVM: ComponentPublicInstance __emitted: Record = {} constructor(vm: ComponentPublicInstance, events: Record) { - this.vm = vm + this.rootVM = vm + this.componentVM = this.rootVM.$refs[ + 'VTU_COMPONENT' + ] as ComponentPublicInstance this.__emitted = events } + private get hasMultipleRoots(): boolean { + // if the subtree is an array of children, we have multiple root nodes + return this.componentVM.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN + } + + get element() { + return this.hasMultipleRoots + ? // get the parent element of the current component + this.componentVM.$el.parentElement + : this.componentVM.$el + } + classes(className?) { - return new DOMWrapper(this.vm.$el).classes(className) + return new DOMWrapper(this.element).classes(className) } attributes(key?: string) { - return new DOMWrapper(this.vm.$el).attributes(key) + return new DOMWrapper(this.element).attributes(key) } exists() { @@ -30,15 +47,17 @@ export class VueWrapper implements WrapperAPI { } html() { - return this.vm.$el.outerHTML + return this.hasMultipleRoots + ? this.element.innerHTML + : this.element.outerHTML } text() { - return this.vm.$el.textContent?.trim() + return this.element.textContent?.trim() } find(selector: string): DOMWrapper | ErrorWrapper { - const result = this.vm.$el.querySelector(selector) as T + const result = this.element.querySelector(selector) as T if (result) { return new DOMWrapper(result) } @@ -47,16 +66,16 @@ export class VueWrapper implements WrapperAPI { } findAll(selector: string): DOMWrapper[] { - const results = (this.vm.$el as Element).querySelectorAll(selector) + const results = (this.element as Element).querySelectorAll(selector) return Array.from(results).map((x) => new DOMWrapper(x)) } async setChecked(checked: boolean = true) { - return new DOMWrapper(this.vm.$el).setChecked(checked) + return new DOMWrapper(this.element).setChecked(checked) } trigger(eventString: string) { - const rootElementWrapper = new DOMWrapper(this.vm.$el) + const rootElementWrapper = new DOMWrapper(this.element) return rootElementWrapper.trigger(eventString) } } diff --git a/tests/find.spec.ts b/tests/find.spec.ts index 8557f5f72..b25df56c3 100644 --- a/tests/find.spec.ts +++ b/tests/find.spec.ts @@ -2,27 +2,56 @@ import { defineComponent, h } from 'vue' import { mount } from '../src' -test('find', () => { - const Component = defineComponent({ - render() { - return h('div', {}, [h('span', { id: 'my-span' })]) - } +describe('find', () => { + test('find using single root node', () => { + const Component = defineComponent({ + render() { + return h('div', {}, [h('span', { id: 'my-span' })]) + } + }) + + const wrapper = mount(Component) + expect(wrapper.find('#my-span')).toBeTruthy() }) - const wrapper = mount(Component) - expect(wrapper.find('#my-span')).toBeTruthy() + it('find using multiple root nodes', () => { + const Component = defineComponent({ + render() { + return [h('div', 'text'), h('span', { id: 'my-span' })] + } + }) + + const wrapper = mount(Component) + expect(wrapper.find('#my-span')).toBeTruthy() + }) }) -test('findAll', () => { - const Component = defineComponent({ - render() { - return h('div', {}, [ - h('span', { className: 'span' }), - h('span', { className: 'span' }) - ]) - } +describe('findAll', () => { + test('findAll using single root node', () => { + const Component = defineComponent({ + render() { + return h('div', {}, [ + h('span', { className: 'span' }), + h('span', { className: 'span' }) + ]) + } + }) + + const wrapper = mount(Component) + expect(wrapper.findAll('.span')).toHaveLength(2) }) - const wrapper = mount(Component) - expect(wrapper.findAll('.span')).toHaveLength(2) -}) \ No newline at end of file + test('findAll using multiple root nodes', () => { + const Component = defineComponent({ + render() { + return [ + h('span', { className: 'span' }), + h('span', { className: 'span' }) + ] + } + }) + + const wrapper = mount(Component) + expect(wrapper.findAll('.span')).toHaveLength(2) + }) +}) diff --git a/tests/html-text.spec.ts b/tests/html-text.spec.ts index edd0edc7f..ab8990654 100644 --- a/tests/html-text.spec.ts +++ b/tests/html-text.spec.ts @@ -2,15 +2,28 @@ import { defineComponent, h } from 'vue' import { mount } from '../src' -test('html, text', () => { - const Component = defineComponent({ - render() { - return h('div', {}, 'Text content') - } +describe('html', () => { + it('returns html when mounting single root node', () => { + const Component = defineComponent({ + render() { + return h('div', {}, 'Text content') + } + }) + + const wrapper = mount(Component) + + expect(wrapper.html()).toBe('

Text content
') }) - const wrapper = mount(Component) + it('returns the html when mounting multiple root nodes', () => { + const Component = defineComponent({ + render() { + return [h('div', {}, 'foo'), h('div', {}, 'bar'), h('div', {}, 'baz')] + } + }) - expect(wrapper.html()).toBe('
Text content
') - expect(wrapper.text()).toBe('Text content') -}) \ No newline at end of file + const wrapper = mount(Component) + + expect(wrapper.html()).toBe('
foo
bar
baz
') + }) +}) diff --git a/tests/setChecked.spec.ts b/tests/setChecked.spec.ts index f40550c44..4146820ea 100644 --- a/tests/setChecked.spec.ts +++ b/tests/setChecked.spec.ts @@ -13,7 +13,7 @@ describe('setChecked', () => { const wrapper = mount(Comp) await wrapper.setChecked() - expect(wrapper.vm.$el.checked).toBe(true) + expect(wrapper.componentVM.$el.checked).toBe(true) }) it('sets element checked true with no option passed', async () => { @@ -64,11 +64,12 @@ describe('setChecked', () => { const listener = jest.fn() const Comp = defineComponent({ setup() { - return () => h('input', { - onChange: listener, - type: 'checkbox', - checked: true - }) + return () => + h('input', { + onChange: listener, + type: 'checkbox', + checked: true + }) } })