Skip to content

Commit 6a21b8c

Browse files
authored
Merge pull request #25 from vuejs/feat/detect-multiple-root-nodes
WIP: detect multiple root nodes
2 parents a5f1d60 + ccb460b commit 6a21b8c

File tree

5 files changed

+117
-56
lines changed

5 files changed

+117
-56
lines changed

src/mount.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {
33
createApp,
44
VNode,
55
defineComponent,
6-
Plugin,
7-
ComponentOptions
6+
VNodeNormalizedChildren,
7+
VNodeProps,
8+
ComponentOptions,
9+
Plugin
810
} from 'vue'
911

1012
import { VueWrapper, createWrapper } from './vue-wrapper'
@@ -41,26 +43,23 @@ export function mount<P>(
4143
document.body.appendChild(el)
4244

4345
// handle any slots passed via mounting options
44-
const slots =
46+
const slots: VNodeNormalizedChildren =
4547
options?.slots &&
46-
Object.entries(options.slots).reduce<Record<string, () => VNode | string>>(
47-
(acc, [name, fn]) => {
48-
acc[name] = () => fn
49-
return acc
50-
},
51-
{}
52-
)
48+
Object.entries(options.slots).reduce((acc, [name, fn]) => {
49+
acc[name] = () => fn
50+
return acc
51+
}, {})
5352
// override component data with mounting options data
5453
if (options?.data) {
5554
const dataMixin = createDataMixin(options.data())
5655
component.mixins = [...(component.mixins || []), dataMixin]
5756
}
5857

5958
// create the wrapper component
60-
const Parent = (props?: P) =>
59+
const Parent = (props?: VNodeProps) =>
6160
defineComponent({
6261
render() {
63-
return h(component, props, slots)
62+
return h(component, { ...props, ref: 'VTU_COMPONENT' }, slots)
6463
}
6564
})
6665

@@ -90,7 +89,7 @@ export function mount<P>(
9089
vm.mixin(emitMixin)
9190

9291
// mount the app!
93-
const app = vm.mount('#app')
92+
const app = vm.mount(el)
9493

9594
return createWrapper(app, events)
9695
}

src/vue-wrapper.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
import { ComponentPublicInstance } from 'vue'
2+
import { ShapeFlags } from '@vue/shared'
23

34
import { DOMWrapper } from './dom-wrapper'
45
import { WrapperAPI } from './types'
56
import { ErrorWrapper } from './error-wrapper'
67

78
export class VueWrapper implements WrapperAPI {
8-
vm: ComponentPublicInstance
9+
rootVM: ComponentPublicInstance
10+
componentVM: ComponentPublicInstance
911
__emitted: Record<string, unknown[]> = {}
1012

1113
constructor(vm: ComponentPublicInstance, events: Record<string, unknown[]>) {
12-
this.vm = vm
14+
this.rootVM = vm
15+
this.componentVM = this.rootVM.$refs[
16+
'VTU_COMPONENT'
17+
] as ComponentPublicInstance
1318
this.__emitted = events
1419
}
1520

21+
private get hasMultipleRoots(): boolean {
22+
// if the subtree is an array of children, we have multiple root nodes
23+
return this.componentVM.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN
24+
}
25+
26+
get element() {
27+
return this.hasMultipleRoots
28+
? // get the parent element of the current component
29+
this.componentVM.$el.parentElement
30+
: this.componentVM.$el
31+
}
32+
1633
classes(className?) {
17-
return new DOMWrapper(this.vm.$el).classes(className)
34+
return new DOMWrapper(this.element).classes(className)
1835
}
1936

2037
attributes(key?: string) {
21-
return new DOMWrapper(this.vm.$el).attributes(key)
38+
return new DOMWrapper(this.element).attributes(key)
2239
}
2340

2441
exists() {
@@ -30,15 +47,17 @@ export class VueWrapper implements WrapperAPI {
3047
}
3148

3249
html() {
33-
return this.vm.$el.outerHTML
50+
return this.hasMultipleRoots
51+
? this.element.innerHTML
52+
: this.element.outerHTML
3453
}
3554

3655
text() {
37-
return this.vm.$el.textContent?.trim()
56+
return this.element.textContent?.trim()
3857
}
3958

4059
find<T extends Element>(selector: string): DOMWrapper<T> | ErrorWrapper {
41-
const result = this.vm.$el.querySelector(selector) as T
60+
const result = this.element.querySelector(selector) as T
4261
if (result) {
4362
return new DOMWrapper(result)
4463
}
@@ -47,16 +66,16 @@ export class VueWrapper implements WrapperAPI {
4766
}
4867

4968
findAll<T extends Element>(selector: string): DOMWrapper<T>[] {
50-
const results = (this.vm.$el as Element).querySelectorAll<T>(selector)
69+
const results = (this.element as Element).querySelectorAll<T>(selector)
5170
return Array.from(results).map((x) => new DOMWrapper(x))
5271
}
5372

5473
async setChecked(checked: boolean = true) {
55-
return new DOMWrapper(this.vm.$el).setChecked(checked)
74+
return new DOMWrapper(this.element).setChecked(checked)
5675
}
5776

5877
trigger(eventString: string) {
59-
const rootElementWrapper = new DOMWrapper(this.vm.$el)
78+
const rootElementWrapper = new DOMWrapper(this.element)
6079
return rootElementWrapper.trigger(eventString)
6180
}
6281
}

tests/find.spec.ts

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,56 @@ import { defineComponent, h } from 'vue'
22

33
import { mount } from '../src'
44

5-
test('find', () => {
6-
const Component = defineComponent({
7-
render() {
8-
return h('div', {}, [h('span', { id: 'my-span' })])
9-
}
5+
describe('find', () => {
6+
test('find using single root node', () => {
7+
const Component = defineComponent({
8+
render() {
9+
return h('div', {}, [h('span', { id: 'my-span' })])
10+
}
11+
})
12+
13+
const wrapper = mount(Component)
14+
expect(wrapper.find('#my-span')).toBeTruthy()
1015
})
1116

12-
const wrapper = mount(Component)
13-
expect(wrapper.find('#my-span')).toBeTruthy()
17+
it('find using multiple root nodes', () => {
18+
const Component = defineComponent({
19+
render() {
20+
return [h('div', 'text'), h('span', { id: 'my-span' })]
21+
}
22+
})
23+
24+
const wrapper = mount(Component)
25+
expect(wrapper.find('#my-span')).toBeTruthy()
26+
})
1427
})
1528

16-
test('findAll', () => {
17-
const Component = defineComponent({
18-
render() {
19-
return h('div', {}, [
20-
h('span', { className: 'span' }),
21-
h('span', { className: 'span' })
22-
])
23-
}
29+
describe('findAll', () => {
30+
test('findAll using single root node', () => {
31+
const Component = defineComponent({
32+
render() {
33+
return h('div', {}, [
34+
h('span', { className: 'span' }),
35+
h('span', { className: 'span' })
36+
])
37+
}
38+
})
39+
40+
const wrapper = mount(Component)
41+
expect(wrapper.findAll('.span')).toHaveLength(2)
2442
})
2543

26-
const wrapper = mount(Component)
27-
expect(wrapper.findAll('.span')).toHaveLength(2)
28-
})
44+
test('findAll using multiple root nodes', () => {
45+
const Component = defineComponent({
46+
render() {
47+
return [
48+
h('span', { className: 'span' }),
49+
h('span', { className: 'span' })
50+
]
51+
}
52+
})
53+
54+
const wrapper = mount(Component)
55+
expect(wrapper.findAll('.span')).toHaveLength(2)
56+
})
57+
})

tests/html-text.spec.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,28 @@ import { defineComponent, h } from 'vue'
22

33
import { mount } from '../src'
44

5-
test('html, text', () => {
6-
const Component = defineComponent({
7-
render() {
8-
return h('div', {}, 'Text content')
9-
}
5+
describe('html', () => {
6+
it('returns html when mounting single root node', () => {
7+
const Component = defineComponent({
8+
render() {
9+
return h('div', {}, 'Text content')
10+
}
11+
})
12+
13+
const wrapper = mount(Component)
14+
15+
expect(wrapper.html()).toBe('<div>Text content</div>')
1016
})
1117

12-
const wrapper = mount(Component)
18+
it('returns the html when mounting multiple root nodes', () => {
19+
const Component = defineComponent({
20+
render() {
21+
return [h('div', {}, 'foo'), h('div', {}, 'bar'), h('div', {}, 'baz')]
22+
}
23+
})
1324

14-
expect(wrapper.html()).toBe('<div>Text content</div>')
15-
expect(wrapper.text()).toBe('Text content')
16-
})
25+
const wrapper = mount(Component)
26+
27+
expect(wrapper.html()).toBe('<div>foo</div><div>bar</div><div>baz</div>')
28+
})
29+
})

tests/setChecked.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('setChecked', () => {
1313
const wrapper = mount(Comp)
1414
await wrapper.setChecked()
1515

16-
expect(wrapper.vm.$el.checked).toBe(true)
16+
expect(wrapper.componentVM.$el.checked).toBe(true)
1717
})
1818

1919
it('sets element checked true with no option passed', async () => {
@@ -64,11 +64,12 @@ describe('setChecked', () => {
6464
const listener = jest.fn()
6565
const Comp = defineComponent({
6666
setup() {
67-
return () => h('input', {
68-
onChange: listener,
69-
type: 'checkbox',
70-
checked: true
71-
})
67+
return () =>
68+
h('input', {
69+
onChange: listener,
70+
type: 'checkbox',
71+
checked: true
72+
})
7273
}
7374
})
7475

0 commit comments

Comments
 (0)