Skip to content

Commit 85c7f52

Browse files
refactor: Refactor the way VueWrapper is created, to allow for wrapping nested instances.
1 parent d49a9b4 commit 85c7f52

8 files changed

+99
-68
lines changed

src/emitMixin.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { getCurrentInstance } from 'vue'
2-
3-
export const createEmitMixin = () => {
4-
const events: Record<string, unknown[]> = {}
1+
import { getCurrentInstance, App } from 'vue'
52

3+
export const attachEventListener = (vm: App) => {
64
const emitMixin = {
75
beforeCreate() {
6+
let events: Record<string, unknown[]> = {}
7+
this.__emitted = events
8+
89
getCurrentInstance().emit = (event: string, ...args: unknown[]) => {
910
events[event]
1011
? (events[event] = [...events[event], [...args]])
@@ -15,8 +16,5 @@ export const createEmitMixin = () => {
1516
}
1617
}
1718

18-
return {
19-
events,
20-
emitMixin
21-
}
19+
vm.mixin(emitMixin)
2220
}

src/error-wrapper.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { FindComponentSelector } from './types'
2+
13
interface Options {
2-
selector: string
4+
selector: FindComponentSelector
35
}
46

57
export class ErrorWrapper {
6-
selector: string
8+
selector: FindComponentSelector
79
element: null
810

911
constructor({ selector }: Options) {
@@ -14,6 +16,10 @@ export class ErrorWrapper {
1416
return Error(`Cannot call ${method} on an empty wrapper.`)
1517
}
1618

19+
vm(): Error {
20+
throw this.wrapperError('vm')
21+
}
22+
1723
attributes() {
1824
throw this.wrapperError('attributes')
1925
}
@@ -34,8 +40,8 @@ export class ErrorWrapper {
3440
throw this.wrapperError('findAll')
3541
}
3642

37-
setChecked() {
38-
throw this.wrapperError('setChecked')
43+
setProps() {
44+
throw this.wrapperError('setProps')
3945
}
4046

4147
setValue() {

src/mount.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import {
99
Plugin,
1010
Directive,
1111
Component,
12-
reactive
12+
reactive,
13+
ComponentPublicInstance
1314
} from 'vue'
1415

1516
import { createWrapper } from './vue-wrapper'
16-
import { createEmitMixin } from './emitMixin'
17+
import { attachEventListener } from './emitMixin'
1718
import { createDataMixin } from './dataMixin'
1819
import { MOUNT_ELEMENT_ID } from './constants'
1920
import { stubComponents } from './stubs'
@@ -134,8 +135,7 @@ export function mount(originalComponent: any, options?: MountingOptions) {
134135
}
135136

136137
// add tracking for emitted events
137-
const { emitMixin, events } = createEmitMixin()
138-
vm.mixin(emitMixin)
138+
attachEventListener(vm)
139139

140140
// stubs
141141
if (options?.global?.stubs) {
@@ -146,6 +146,6 @@ export function mount(originalComponent: any, options?: MountingOptions) {
146146

147147
// mount the app!
148148
const app = vm.mount(el)
149-
150-
return createWrapper(app, events, setProps)
149+
const App = app.$refs['VTU_COMPONENT'] as ComponentPublicInstance
150+
return createWrapper(App, setProps)
151151
}

src/vue-wrapper.ts

+29-31
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,35 @@ import {
88
WrapperAPI
99
} from './types'
1010
import { ErrorWrapper } from './error-wrapper'
11-
import { MOUNT_ELEMENT_ID } from './constants'
1211
import { find } from './utils/find'
1312

1413
export class VueWrapper implements WrapperAPI {
1514
private componentVM: ComponentPublicInstance
16-
private __emitted: Record<string, unknown[]> = {}
17-
private __vm: ComponentPublicInstance
15+
private rootVM: ComponentPublicInstance
1816
private __setProps: (props: Record<string, any>) => void
1917

2018
constructor(
2119
vm: ComponentPublicInstance,
22-
events: Record<string, unknown[]>,
23-
setProps: (props: Record<string, any>) => void
20+
setProps?: (props: Record<string, any>) => void
2421
) {
25-
this.__vm = vm
22+
// TODO Remove cast after Vue releases the fix
23+
this.rootVM = (vm.$root as any) as ComponentPublicInstance
24+
this.componentVM = vm
2625
this.__setProps = setProps
27-
this.componentVM = this.__vm.$refs[
28-
'VTU_COMPONENT'
29-
] as ComponentPublicInstance
30-
this.__emitted = events
31-
}
32-
33-
private get appRootNode() {
34-
return document.getElementById(MOUNT_ELEMENT_ID) as HTMLDivElement
3526
}
3627

3728
private get hasMultipleRoots(): boolean {
3829
// if the subtree is an array of children, we have multiple root nodes
39-
return this.componentVM.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN
30+
return this.vm.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN
4031
}
4132

4233
private get parentElement(): Element {
43-
return this.componentVM.$el.parentElement
34+
return this.vm.$el.parentElement
4435
}
4536

4637
get element(): Element {
4738
// if the component has multiple root elements, we use the parent's element
48-
return this.hasMultipleRoots ? this.parentElement : this.componentVM.$el
39+
return this.hasMultipleRoots ? this.parentElement : this.vm.$el
4940
}
5041

5142
get vm(): ComponentPublicInstance {
@@ -64,8 +55,10 @@ export class VueWrapper implements WrapperAPI {
6455
return true
6556
}
6657

67-
emitted() {
68-
return this.__emitted
58+
emitted(): Record<string, unknown[]> {
59+
// TODO Should we define this?
60+
// @ts-ignore
61+
return this.vm.__emitted
6962
}
7063

7164
html() {
@@ -95,26 +88,32 @@ export class VueWrapper implements WrapperAPI {
9588
return result
9689
}
9790

98-
findComponent(selector: FindComponentSelector): ComponentPublicInstance {
91+
findComponent(selector: FindComponentSelector): VueWrapper | ErrorWrapper {
9992
if (typeof selector === 'object' && 'ref' in selector) {
100-
return this.componentVM.$refs[selector.ref] as ComponentPublicInstance
93+
return createWrapper(
94+
this.vm.$refs[selector.ref] as ComponentPublicInstance
95+
)
10196
}
102-
const result = find(this.componentVM.$.subTree, selector)
103-
return result.length ? result[0] : undefined
97+
const result = find(this.vm.$.subTree, selector)
98+
if (!result.length) return new ErrorWrapper({ selector })
99+
return createWrapper(result[0])
104100
}
105101

106-
findAllComponents(
107-
selector: FindAllComponentsSelector
108-
): ComponentPublicInstance[] {
109-
return find(this.componentVM.$.subTree, selector)
102+
findAllComponents(selector: FindAllComponentsSelector): VueWrapper[] {
103+
return find(this.vm.$.subTree, selector).map((c) => createWrapper(c))
110104
}
111105

112106
findAll<T extends Element>(selector: string): DOMWrapper<T>[] {
113107
const results = this.parentElement.querySelectorAll<T>(selector)
114108
return Array.from(results).map((x) => new DOMWrapper(x))
115109
}
116110

117-
setProps(props: Record<string, any>) {
111+
setProps(props: Record<string, any>): Promise<void> {
112+
// if this VM's parent is not the root, error out
113+
// TODO: Remove ignore after Vue releases fix
114+
// @ts-ignore
115+
if (this.vm.$parent !== this.rootVM)
116+
throw Error('You can only use setProps on your mounted component')
118117
this.__setProps(props)
119118
return nextTick()
120119
}
@@ -127,8 +126,7 @@ export class VueWrapper implements WrapperAPI {
127126

128127
export function createWrapper(
129128
vm: ComponentPublicInstance,
130-
events: Record<string, unknown[]>,
131-
setProps: (props: Record<string, any>) => void
129+
setProps?: (props: Record<string, any>) => void
132130
): VueWrapper {
133-
return new VueWrapper(vm, events, setProps)
131+
return new VueWrapper(vm, setProps)
134132
}

tests/findAllComponents.spec.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ describe('findAllComponents', () => {
1919
const wrapper = mount(compA)
2020
// find by DOM selector
2121
expect(wrapper.findAllComponents('.C')).toHaveLength(2)
22-
expect(
23-
wrapper.findAllComponents({ name: 'Hello' })[0].$el.textContent
24-
).toBe('Hello world')
25-
expect(wrapper.findAllComponents(Hello)[0].$el.textContent).toBe(
22+
expect(wrapper.findAllComponents({ name: 'Hello' })[0].text()).toBe(
2623
'Hello world'
2724
)
25+
expect(wrapper.findAllComponents(Hello)[0].text()).toBe('Hello world')
2826
})
2927
})

tests/findComponent.spec.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const compA = {
3737
describe('findComponent', () => {
3838
it('does not find plain dom elements', () => {
3939
const wrapper = mount(compA)
40-
expect(wrapper.findComponent('.domElement')).toBeFalsy()
40+
expect(wrapper.findComponent('.domElement').exists()).toBeFalsy()
4141
})
4242

4343
it('finds component by ref', () => {
@@ -49,34 +49,39 @@ describe('findComponent', () => {
4949
it('finds component by dom selector', () => {
5050
const wrapper = mount(compA)
5151
// find by DOM selector
52-
expect(wrapper.findComponent('.C').$options.name).toEqual('ComponentC')
52+
expect(wrapper.findComponent('.C').vm).toHaveProperty(
53+
'$options.name',
54+
'ComponentC'
55+
)
5356
})
5457

5558
it('does allows using complicated DOM selector query', () => {
5659
const wrapper = mount(compA)
57-
expect(wrapper.findComponent('.B > .C').$options.name).toEqual('ComponentC')
60+
expect(wrapper.findComponent('.B > .C').vm).toHaveProperty(
61+
'$options.name',
62+
'ComponentC'
63+
)
5864
})
5965

6066
it('finds a component when root of mounted component', async () => {
6167
const wrapper = mount(compD)
6268
// make sure it finds the component, not its root
63-
expect(wrapper.findComponent('.c-as-root-on-d').$options.name).toEqual(
69+
expect(wrapper.findComponent('.c-as-root-on-d').vm).toHaveProperty(
70+
'$options.name',
6471
'ComponentC'
6572
)
6673
})
6774

6875
it('finds component by name', () => {
6976
const wrapper = mount(compA)
70-
expect(wrapper.findComponent({ name: 'Hello' }).$el.textContent).toBe(
71-
'Hello world'
72-
)
73-
expect(wrapper.findComponent({ name: 'ComponentB' })).toBeTruthy()
74-
expect(wrapper.findComponent({ name: 'component-c' })).toBeTruthy()
77+
expect(wrapper.findComponent({ name: 'Hello' }).text()).toBe('Hello world')
78+
expect(wrapper.findComponent({ name: 'ComponentB' }).exists()).toBeTruthy()
79+
expect(wrapper.findComponent({ name: 'component-c' }).exists()).toBeTruthy()
7580
})
7681

7782
it('finds component by imported SFC file', () => {
7883
const wrapper = mount(compA)
79-
expect(wrapper.findComponent(Hello).$el.textContent).toBe('Hello world')
80-
expect(wrapper.findComponent(compC).$el.textContent).toBe('C')
84+
expect(wrapper.findComponent(Hello).text()).toBe('Hello world')
85+
expect(wrapper.findComponent(compC).text()).toBe('C')
8186
})
8287
})

tests/setProps.spec.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ describe('setProps', () => {
1010
}
1111
const wrapper = mount(Foo, {
1212
props: {
13-
foo: 'foo'
13+
foo: 'bar'
1414
}
1515
})
16-
expect(wrapper.html()).toContain('foo')
16+
expect(wrapper.html()).toContain('bar')
1717

1818
await wrapper.setProps({ foo: 'qux' })
1919
expect(wrapper.html()).toContain('qux')
@@ -44,7 +44,8 @@ describe('setProps', () => {
4444
it('sets component props, and updates DOM when props were not initially passed', async () => {
4545
const Foo = {
4646
props: ['foo'],
47-
template: `<div>{{ foo }}</div>`
47+
template: `
48+
<div>{{ foo }}</div>`
4849
}
4950
const wrapper = mount(Foo)
5051
expect(wrapper.html()).not.toContain('foo')
@@ -67,7 +68,8 @@ describe('setProps', () => {
6768
this.bar = val
6869
}
6970
},
70-
template: `<div>{{ bar }}</div>`
71+
template: `
72+
<div>{{ bar }}</div>`
7173
}
7274
const wrapper = mount(Foo)
7375
expect(wrapper.html()).toContain('original-bar')
@@ -118,4 +120,27 @@ describe('setProps', () => {
118120
expect(wrapper.attributes()).toEqual(nonExistentProp)
119121
expect(wrapper.html()).toBe('<div bar="qux">foo</div>')
120122
})
123+
124+
it('allows using only on mounted component', async () => {
125+
const Foo = {
126+
name: 'Foo',
127+
props: ['foo'],
128+
template: '<div>{{ foo }}</div>'
129+
}
130+
const Baz = {
131+
props: ['baz'],
132+
template: '<div><Foo :foo="baz"/></div>',
133+
components: { Foo }
134+
}
135+
136+
const wrapper = mount(Baz, {
137+
props: {
138+
baz: 'baz'
139+
}
140+
})
141+
const FooResult = wrapper.findComponent({ name: 'Foo' })
142+
expect(() => FooResult.setProps({ baz: 'bin' })).toThrowError(
143+
'You can only use setProps on your mounted component'
144+
)
145+
})
121146
})

tests/vm.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mount } from '../src'
55
describe('vm', () => {
66
it('returns the component vm', () => {
77
const Component = defineComponent({
8+
name: 'VTUComponent',
89
template: '<div>{{ msg }}</div>',
910
setup() {
1011
const msg = 'hello'

0 commit comments

Comments
 (0)