Skip to content

Commit 1f31a51

Browse files
refactor: Refactor the way VueWrapper is created, to allow for wrapping nested instances.
1 parent 92de550 commit 1f31a51

File tree

8 files changed

+97
-65
lines changed

8 files changed

+97
-65
lines changed

src/emitMixin.ts

Lines changed: 6 additions & 8 deletions
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

Lines changed: 10 additions & 4 deletions
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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from 'vue'
1515

1616
import { createWrapper, VueWrapper } from './vue-wrapper'
17-
import { createEmitMixin } from './emitMixin'
17+
import { attachEventListener } from './emitMixin'
1818
import { createDataMixin } from './dataMixin'
1919
import { MOUNT_ELEMENT_ID } from './constants'
2020
import { stubComponents } from './stubs'
@@ -145,8 +145,7 @@ export function mount<T extends ComponentPublicInstance>(
145145
}
146146

147147
// add tracking for emitted events
148-
const { emitMixin, events } = createEmitMixin()
149-
vm.mixin(emitMixin)
148+
attachEventListener(vm)
150149

151150
// stubs
152151
if (options?.global?.stubs) {
@@ -157,6 +156,6 @@ export function mount<T extends ComponentPublicInstance>(
157156

158157
// mount the app!
159158
const app = vm.mount(el)
160-
161-
return createWrapper<T>(app, events, setProps)
159+
const App = app.$refs['VTU_COMPONENT'] as ComponentPublicInstance
160+
return createWrapper<T>(App, setProps)
162161
}

src/vue-wrapper.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,36 @@ 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<T extends ComponentPublicInstance>
1514
implements WrapperAPI {
1615
private componentVM: T
17-
private __emitted: Record<string, unknown[]> = {}
18-
private __vm: ComponentPublicInstance
16+
private rootVM: ComponentPublicInstance
1917
private __setProps: (props: Record<string, any>) => void
2018

2119
constructor(
2220
vm: ComponentPublicInstance,
23-
events: Record<string, unknown[]>,
24-
setProps: (props: Record<string, any>) => void
21+
setProps?: (props: Record<string, any>) => void
2522
) {
26-
this.__vm = vm
23+
// TODO Remove cast after Vue releases the fix
24+
this.rootVM = (vm.$root as any) as ComponentPublicInstance
25+
this.componentVM = vm as T
2726
this.__setProps = setProps
28-
this.componentVM = this.__vm.$refs['VTU_COMPONENT'] as T
29-
this.__emitted = events
30-
}
31-
32-
private get appRootNode() {
33-
return document.getElementById(MOUNT_ELEMENT_ID) as HTMLDivElement
3427
}
3528

3629
private get hasMultipleRoots(): boolean {
3730
// if the subtree is an array of children, we have multiple root nodes
38-
return this.componentVM.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN
31+
return this.vm.$.subTree.shapeFlag === ShapeFlags.ARRAY_CHILDREN
3932
}
4033

4134
private get parentElement(): Element {
42-
return this.componentVM.$el.parentElement
35+
return this.vm.$el.parentElement
4336
}
4437

4538
get element(): Element {
4639
// if the component has multiple root elements, we use the parent's element
47-
return this.hasMultipleRoots ? this.parentElement : this.componentVM.$el
40+
return this.hasMultipleRoots ? this.parentElement : this.vm.$el
4841
}
4942

5043
get vm(): T {
@@ -63,8 +56,10 @@ export class VueWrapper<T extends ComponentPublicInstance>
6356
return true
6457
}
6558

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

7065
html() {
@@ -94,26 +89,32 @@ export class VueWrapper<T extends ComponentPublicInstance>
9489
return result
9590
}
9691

97-
findComponent(selector: FindComponentSelector): ComponentPublicInstance {
92+
findComponent(selector: FindComponentSelector): VueWrapper | ErrorWrapper {
9893
if (typeof selector === 'object' && 'ref' in selector) {
99-
return this.componentVM.$refs[selector.ref] as ComponentPublicInstance
94+
return createWrapper(
95+
this.vm.$refs[selector.ref] as ComponentPublicInstance
96+
)
10097
}
101-
const result = find(this.componentVM.$.subTree, selector)
102-
return result.length ? result[0] : undefined
98+
const result = find(this.vm.$.subTree, selector)
99+
if (!result.length) return new ErrorWrapper({ selector })
100+
return createWrapper(result[0])
103101
}
104102

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

111107
findAll<T extends Element>(selector: string): DOMWrapper<T>[] {
112108
const results = this.parentElement.querySelectorAll<T>(selector)
113109
return Array.from(results).map((x) => new DOMWrapper(x))
114110
}
115111

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

127128
export function createWrapper<T extends ComponentPublicInstance>(
128129
vm: ComponentPublicInstance,
129-
events: Record<string, unknown[]>,
130-
setProps: (props: Record<string, any>) => void
130+
setProps?: (props: Record<string, any>) => void
131131
): VueWrapper<T> {
132-
return new VueWrapper<T>(vm, events, setProps)
132+
return new VueWrapper<T>(vm, setProps)
133133
}

tests/findAllComponents.spec.ts

Lines changed: 2 additions & 4 deletions
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

Lines changed: 16 additions & 11 deletions
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

Lines changed: 29 additions & 4 deletions
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

Lines changed: 1 addition & 0 deletions
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)