From 1cf1522bf29de1eefba2d2c95040d3e8c5e2d548 Mon Sep 17 00:00:00 2001 From: dobromir-hristov Date: Fri, 27 Mar 2020 15:57:14 +0200 Subject: [PATCH 1/5] feat: detect multiple root nodes --- src/mount.ts | 2 +- src/vue-wrapper.ts | 26 +++++++++++++++++--------- tests/multipleRootNodes.spec.ts | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 tests/multipleRootNodes.spec.ts diff --git a/src/mount.ts b/src/mount.ts index adf0273c1..8ac8c0e0e 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -90,7 +90,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..914a6ff45 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,18 +1,26 @@ -import { ComponentPublicInstance } from 'vue' +import { ComponentPublicInstance, ComponentInternalInstance } 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: ComponentInternalInstance __emitted: Record = {} constructor(vm: ComponentPublicInstance, events: Record) { - this.vm = vm + this.rootVM = vm + this.componentVM = this.rootVM.$.subTree.component this.__emitted = events } + get __hasMultipleRoots(): boolean { + // if the subtree is an array of children, we have multiple root nodes + return this.componentVM.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN + } + classes(className?) { return new DOMWrapper(this.vm.$el).classes(className) } @@ -30,15 +38,15 @@ export class VueWrapper implements WrapperAPI { } html() { - return this.vm.$el.outerHTML + return this.rootVM.$el.outerHTML } text() { - return this.vm.$el.textContent?.trim() + return this.rootVM.$el.textContent?.trim() } find(selector: string): DOMWrapper | ErrorWrapper { - const result = this.vm.$el.querySelector(selector) as T + const result = this.rootVM.$el.querySelector(selector) as T if (result) { return new DOMWrapper(result) } @@ -47,16 +55,16 @@ export class VueWrapper implements WrapperAPI { } findAll(selector: string): DOMWrapper[] { - const results = (this.vm.$el as Element).querySelectorAll(selector) + const results = (this.rootVM.$el 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.rootVM.$el).setChecked(checked) } trigger(eventString: string) { - const rootElementWrapper = new DOMWrapper(this.vm.$el) + const rootElementWrapper = new DOMWrapper(this.rootVM.$el) return rootElementWrapper.trigger(eventString) } } diff --git a/tests/multipleRootNodes.spec.ts b/tests/multipleRootNodes.spec.ts new file mode 100644 index 000000000..e8970c308 --- /dev/null +++ b/tests/multipleRootNodes.spec.ts @@ -0,0 +1,24 @@ +import { h } from 'vue' +import { mount } from '../src' + +describe('multipleRootNodes', () => { + it('returns false if only one root node', () => { + const component = { + render() { + return h('div') + } + } + const wrapper = mount(component) + expect(wrapper.__hasMultipleRoots).toBe(false) + }) + + it('returns true if multiple root nodes', () => { + const component = { + render() { + return [h('div', '1'), h('div', '2')] + } + } + const wrapper = mount(component) + expect(wrapper.__hasMultipleRoots).toBe(true) + }) +}) From eb2c64c9e9d64333b88edf4a622cf1a7dc3a07ab Mon Sep 17 00:00:00 2001 From: dobromir-hristov Date: Fri, 27 Mar 2020 17:41:38 +0200 Subject: [PATCH 2/5] refactor: use ref instead of reaching in --- src/mount.ts | 2 +- src/vue-wrapper.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index 8ac8c0e0e..b01e85237 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -60,7 +60,7 @@ export function mount

( const Parent = (props?: P) => defineComponent({ render() { - return h(component, props, slots) + return h(component, { ...props, ref: 'VTU_COMPONENT' }, slots) } }) diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 914a6ff45..69215c67a 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,4 +1,4 @@ -import { ComponentPublicInstance, ComponentInternalInstance } from 'vue' +import { ComponentPublicInstance } from 'vue' import { ShapeFlags } from '@vue/shared' import { DOMWrapper } from './dom-wrapper' @@ -7,18 +7,20 @@ import { ErrorWrapper } from './error-wrapper' export class VueWrapper implements WrapperAPI { rootVM: ComponentPublicInstance - componentVM: ComponentInternalInstance + componentVM: ComponentPublicInstance __emitted: Record = {} constructor(vm: ComponentPublicInstance, events: Record) { this.rootVM = vm - this.componentVM = this.rootVM.$.subTree.component + this.componentVM = this.rootVM.$refs[ + 'VTU_COMPONENT' + ] as ComponentPublicInstance this.__emitted = events } get __hasMultipleRoots(): boolean { // if the subtree is an array of children, we have multiple root nodes - return this.componentVM.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN + return this.componentVM.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN } classes(className?) { From bcfb13d1be482ceba7d29a401562559f1cb9ed07 Mon Sep 17 00:00:00 2001 From: dobromir-hristov Date: Fri, 27 Mar 2020 17:42:29 +0200 Subject: [PATCH 3/5] refactor: fix types --- src/mount.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index b01e85237..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,7 +56,7 @@ export function mount

( } // create the wrapper component - const Parent = (props?: P) => + const Parent = (props?: VNodeProps) => defineComponent({ render() { return h(component, { ...props, ref: 'VTU_COMPONENT' }, slots) From 6d6e7aee8cbf2a6c58bfed39bf7b443436491d01 Mon Sep 17 00:00:00 2001 From: dobromir-hristov Date: Fri, 27 Mar 2020 18:30:33 +0200 Subject: [PATCH 4/5] feat: fallback to parent element if current element has multiple roots. --- src/vue-wrapper.ts | 23 +++++++++----- tests/find.spec.ts | 65 +++++++++++++++++++++++++++++----------- tests/setChecked.spec.ts | 13 ++++---- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 69215c67a..a7cab2712 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -23,12 +23,19 @@ export class VueWrapper implements WrapperAPI { 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() { @@ -40,15 +47,15 @@ export class VueWrapper implements WrapperAPI { } html() { - return this.rootVM.$el.outerHTML + return this.element.outerHTML } text() { - return this.rootVM.$el.textContent?.trim() + return this.element.textContent?.trim() } find(selector: string): DOMWrapper | ErrorWrapper { - const result = this.rootVM.$el.querySelector(selector) as T + const result = this.element.querySelector(selector) as T if (result) { return new DOMWrapper(result) } @@ -57,16 +64,16 @@ export class VueWrapper implements WrapperAPI { } findAll(selector: string): DOMWrapper[] { - const results = (this.rootVM.$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.rootVM.$el).setChecked(checked) + return new DOMWrapper(this.element).setChecked(checked) } trigger(eventString: string) { - const rootElementWrapper = new DOMWrapper(this.rootVM.$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/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 + }) } }) From ccb460be55f9f6be05ab708500a41ec8adf6f4bc Mon Sep 17 00:00:00 2001 From: dobromir-hristov Date: Sun, 29 Mar 2020 12:33:47 +0300 Subject: [PATCH 5/5] refactor: make hasMultipleRoots a private property --- src/vue-wrapper.ts | 8 +++++--- tests/html-text.spec.ts | 31 ++++++++++++++++++++++--------- tests/multipleRootNodes.spec.ts | 24 ------------------------ 3 files changed, 27 insertions(+), 36 deletions(-) delete mode 100644 tests/multipleRootNodes.spec.ts diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index a7cab2712..3ba359f3a 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -18,13 +18,13 @@ export class VueWrapper implements WrapperAPI { this.__emitted = events } - get __hasMultipleRoots(): boolean { + 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 + return this.hasMultipleRoots ? // get the parent element of the current component this.componentVM.$el.parentElement : this.componentVM.$el @@ -47,7 +47,9 @@ export class VueWrapper implements WrapperAPI { } html() { - return this.element.outerHTML + return this.hasMultipleRoots + ? this.element.innerHTML + : this.element.outerHTML } text() { 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/multipleRootNodes.spec.ts b/tests/multipleRootNodes.spec.ts deleted file mode 100644 index e8970c308..000000000 --- a/tests/multipleRootNodes.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { h } from 'vue' -import { mount } from '../src' - -describe('multipleRootNodes', () => { - it('returns false if only one root node', () => { - const component = { - render() { - return h('div') - } - } - const wrapper = mount(component) - expect(wrapper.__hasMultipleRoots).toBe(false) - }) - - it('returns true if multiple root nodes', () => { - const component = { - render() { - return [h('div', '1'), h('div', '2')] - } - } - const wrapper = mount(component) - expect(wrapper.__hasMultipleRoots).toBe(true) - }) -})