Skip to content

Commit 4fefe74

Browse files
authored
Merge pull request #78 from pikax/feat/unmount
feat: add unmount to wrapper
2 parents 9bc919c + 3f78533 commit 4fefe74

File tree

6 files changed

+94
-20
lines changed

6 files changed

+94
-20
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ trigger | ✅ | returns `nextTick`. You can do `await wrapper.find('button').tri
8787
setProps | ✅ |
8888
props | ✅
8989
setData | ❌ | has PR
90-
destroy | ❌
90+
destroy | ✅ | renamed to `unmount` to match Vue 3 lifecycle hook name.
91+
props | ❌
9192
contains | ⚰️| use `find`
9293
emittedByOrder | ⚰️ | use `emitted`
9394
setSelected | ⚰️ | now part of `setValue`

src/error-wrapper.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ export class ErrorWrapper {
5555
trigger() {
5656
throw this.wrapperError('trigger')
5757
}
58+
59+
unmount() {
60+
throw this.wrapperError('unmount')
61+
}
5862
}

src/mount.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ export function mount(
105105
props[k] = v
106106
}
107107

108-
return app.$nextTick()
108+
return vm.$nextTick()
109109
}
110110

111-
// create the vm
112-
const vm = createApp(Parent)
111+
// create the app
112+
const app = createApp(Parent)
113113

114114
// global mocks mixin
115115
if (options?.global?.mocks) {
@@ -121,39 +121,39 @@ export function mount(
121121
}
122122
}
123123

124-
vm.mixin(mixin)
124+
app.mixin(mixin)
125125
}
126126

127127
// use and plugins from mounting options
128128
if (options?.global?.plugins) {
129-
for (const use of options?.global?.plugins) vm.use(use)
129+
for (const use of options?.global?.plugins) app.use(use)
130130
}
131131

132132
// use any mixins from mounting options
133133
if (options?.global?.mixins) {
134-
for (const mixin of options?.global?.mixins) vm.mixin(mixin)
134+
for (const mixin of options?.global?.mixins) app.mixin(mixin)
135135
}
136136

137137
if (options?.global?.components) {
138138
for (const key of Object.keys(options?.global?.components))
139-
vm.component(key, options.global.components[key])
139+
app.component(key, options.global.components[key])
140140
}
141141

142142
if (options?.global?.directives) {
143143
for (const key of Object.keys(options?.global?.directives))
144-
vm.directive(key, options.global.directives[key])
144+
app.directive(key, options.global.directives[key])
145145
}
146146

147147
// provide any values passed via provides mounting option
148148
if (options?.global?.provide) {
149149
for (const key of Reflect.ownKeys(options.global.provide)) {
150150
// @ts-ignore: https://github.com/microsoft/TypeScript/issues/1863
151-
vm.provide(key, options.global.provide[key])
151+
app.provide(key, options.global.provide[key])
152152
}
153153
}
154154

155155
// add tracking for emitted events
156-
vm.mixin(attachEmitListener())
156+
app.mixin(attachEmitListener())
157157

158158
// stubs
159159
if (options?.global?.stubs) {
@@ -163,7 +163,8 @@ export function mount(
163163
}
164164

165165
// mount the app!
166-
const app = vm.mount(el)
167-
const App = app.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance
168-
return createWrapper(App, setProps)
166+
const vm = app.mount(el)
167+
168+
const App = vm.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance
169+
return createWrapper(app, App, setProps)
169170
}

src/vue-wrapper.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComponentPublicInstance, nextTick } from 'vue'
1+
import { ComponentPublicInstance, nextTick, App, render } from 'vue'
22
import { ShapeFlags } from '@vue/shared'
33

44
import { DOMWrapper } from './dom-wrapper'
@@ -14,12 +14,15 @@ export class VueWrapper<T extends ComponentPublicInstance>
1414
implements WrapperAPI {
1515
private componentVM: T
1616
private rootVM: ComponentPublicInstance
17+
private __app: App | null
1718
private __setProps: (props: Record<string, any>) => void
1819

1920
constructor(
21+
app: App | null,
2022
vm: ComponentPublicInstance,
2123
setProps?: (props: Record<string, any>) => void
2224
) {
25+
this.__app = app
2326
this.rootVM = vm.$root
2427
this.componentVM = vm as T
2528
this.__setProps = setProps
@@ -96,15 +99,15 @@ export class VueWrapper<T extends ComponentPublicInstance>
9699

97100
findComponent(selector: FindComponentSelector): VueWrapper<T> | ErrorWrapper {
98101
if (typeof selector === 'object' && 'ref' in selector) {
99-
return createWrapper(this.vm.$refs[selector.ref] as T)
102+
return createWrapper(null, this.vm.$refs[selector.ref] as T)
100103
}
101104
const result = find(this.vm.$.subTree, selector)
102105
if (!result.length) return new ErrorWrapper({ selector })
103-
return createWrapper(result[0])
106+
return createWrapper(null, result[0])
104107
}
105108

106109
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<T>[] {
107-
return find(this.vm.$.subTree, selector).map((c) => createWrapper(c))
110+
return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c))
108111
}
109112

110113
findAll<T extends Element>(selector: string): DOMWrapper<T>[] {
@@ -125,11 +128,26 @@ export class VueWrapper<T extends ComponentPublicInstance>
125128
const rootElementWrapper = new DOMWrapper(this.element)
126129
return rootElementWrapper.trigger(eventString)
127130
}
131+
132+
unmount() {
133+
// preventing dispose of child component
134+
if (!this.__app) {
135+
throw new Error(
136+
`wrapper.unmount() can only be called by the root wrapper`
137+
)
138+
}
139+
140+
if (this.parentElement) {
141+
this.parentElement.removeChild(this.element)
142+
}
143+
this.__app.unmount(this.element)
144+
}
128145
}
129146

130147
export function createWrapper<T extends ComponentPublicInstance>(
148+
app: App,
131149
vm: ComponentPublicInstance,
132150
setProps?: (props: Record<string, any>) => void
133151
): VueWrapper<T> {
134-
return new VueWrapper<T>(vm, setProps)
152+
return new VueWrapper<T>(app, vm, setProps)
135153
}

tests/findComponent.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,9 @@ describe('findComponent', () => {
8686
expect(wrapper.findComponent(Hello).text()).toBe('Hello world')
8787
expect(wrapper.findComponent(compC).text()).toBe('C')
8888
})
89+
90+
it('throw error if trying to unmount component from find', () => {
91+
const wrapper = mount(compA)
92+
expect(wrapper.findComponent(Hello).unmount).toThrowError()
93+
})
8994
})

tests/lifecycle.spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { defineComponent, h, onMounted, nextTick, onBeforeMount } from 'vue'
1+
import {
2+
defineComponent,
3+
h,
4+
onMounted,
5+
nextTick,
6+
onBeforeMount,
7+
onUnmounted,
8+
onBeforeUnmount,
9+
ref
10+
} from 'vue'
211

312
import { mount } from '../src'
413

@@ -24,4 +33,40 @@ describe('lifecycles', () => {
2433
expect(onBeforeMountFn).toHaveBeenCalled()
2534
expect(onBeforeMountFn).toHaveBeenCalled()
2635
})
36+
37+
it('calls onUnmounted', async () => {
38+
const beforeUnmountFn = jest.fn()
39+
const onBeforeUnmountFn = jest.fn()
40+
const onUnmountFn = jest.fn()
41+
const Component = defineComponent({
42+
beforeUnmount: beforeUnmountFn,
43+
setup() {
44+
onUnmounted(onUnmountFn)
45+
onBeforeUnmount(onBeforeUnmountFn)
46+
47+
return () => h('div')
48+
}
49+
})
50+
51+
const wrapper = mount(Component)
52+
await nextTick()
53+
expect(beforeUnmountFn).not.toHaveBeenCalled()
54+
expect(onBeforeUnmountFn).not.toHaveBeenCalled()
55+
expect(onUnmountFn).not.toHaveBeenCalled()
56+
57+
const removeChildSpy = jest.spyOn(
58+
wrapper.element.parentElement,
59+
'removeChild'
60+
)
61+
62+
const el = wrapper.element
63+
64+
wrapper.unmount()
65+
66+
expect(beforeUnmountFn).toHaveBeenCalled()
67+
expect(onBeforeUnmountFn).toHaveBeenCalled()
68+
expect(onUnmountFn).toHaveBeenCalled()
69+
70+
expect(removeChildSpy).toHaveBeenCalledWith(el)
71+
})
2772
})

0 commit comments

Comments
 (0)