From fcde128c85cbe37c17875c625dc37eaac3fb4c5d Mon Sep 17 00:00:00 2001 From: pikax Date: Sat, 18 Apr 2020 09:27:29 +0100 Subject: [PATCH 1/3] feat: add unmount to wrapper --- src/mount.ts | 24 ++++++++++++------------ src/vue-wrapper.ts | 15 +++++++++++++-- tests/lifecycle.spec.ts | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index 684cd09a3..70e1ca1ee 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -101,11 +101,11 @@ export function mount( props[k] = v } - return app.$nextTick() + return vm.$nextTick() } - // create the vm - const vm = createApp(Parent) + // create the app + const app = createApp(Parent) // global mocks mixin if (options?.global?.mocks) { @@ -117,40 +117,40 @@ export function mount( } } - vm.mixin(mixin) + app.mixin(mixin) } // use and plugins from mounting options if (options?.global?.plugins) { - for (const use of options?.global?.plugins) vm.use(use) + for (const use of options?.global?.plugins) app.use(use) } // use any mixins from mounting options if (options?.global?.mixins) { - for (const mixin of options?.global?.mixins) vm.mixin(mixin) + for (const mixin of options?.global?.mixins) app.mixin(mixin) } if (options?.global?.components) { for (const key of Object.keys(options?.global?.components)) - vm.component(key, options.global.components[key]) + app.component(key, options.global.components[key]) } if (options?.global?.directives) { for (const key of Object.keys(options?.global?.directives)) - vm.directive(key, options.global.directives[key]) + app.directive(key, options.global.directives[key]) } // provide any values passed via provides mounting option if (options?.global?.provide) { for (const key of Reflect.ownKeys(options.global.provide)) { // @ts-ignore: https://github.com/microsoft/TypeScript/issues/1863 - vm.provide(key, options.global.provide[key]) + app.provide(key, options.global.provide[key]) } } // add tracking for emitted events const { emitMixin, events } = createEmitMixin() - vm.mixin(emitMixin) + app.mixin(emitMixin) // stubs if (options?.global?.stubs) { @@ -160,7 +160,7 @@ export function mount( } // mount the app! - const app = vm.mount(el) + const vm = app.mount(el) - return createWrapper(app, events, setProps) + return createWrapper(app, vm, events, setProps) } diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 0e857cca7..9c858e3c9 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,4 +1,4 @@ -import { ComponentPublicInstance, nextTick } from 'vue' +import { ComponentPublicInstance, nextTick, App } from 'vue' import { ShapeFlags } from '@vue/shared' import { DOMWrapper } from './dom-wrapper' @@ -9,15 +9,18 @@ import { MOUNT_ELEMENT_ID } from './constants' export class VueWrapper implements WrapperAPI { private componentVM: T + private __app: App private __emitted: Record = {} private __vm: ComponentPublicInstance private __setProps: (props: Record) => void constructor( + app: App, vm: ComponentPublicInstance, events: Record, setProps: (props: Record) => void ) { + this.__app = app this.__vm = vm this.__setProps = setProps this.componentVM = this.__vm.$refs['VTU_COMPONENT'] as T @@ -109,12 +112,20 @@ export class VueWrapper const rootElementWrapper = new DOMWrapper(this.element) return rootElementWrapper.trigger(eventString) } + + unmount() { + if (this.parentElement) { + this.parentElement.removeChild(this.element) + } + this.__app.unmount(this.element) + } } export function createWrapper( + app: App, vm: ComponentPublicInstance, events: Record, setProps: (props: Record) => void ): VueWrapper { - return new VueWrapper(vm, events, setProps) + return new VueWrapper(app, vm, events, setProps) } diff --git a/tests/lifecycle.spec.ts b/tests/lifecycle.spec.ts index 97a116850..3889121f5 100644 --- a/tests/lifecycle.spec.ts +++ b/tests/lifecycle.spec.ts @@ -1,4 +1,12 @@ -import { defineComponent, h, onMounted, nextTick, onBeforeMount } from 'vue' +import { + defineComponent, + h, + onMounted, + nextTick, + onBeforeMount, + onUnmounted, + onBeforeUnmount +} from 'vue' import { mount } from '../src' @@ -24,4 +32,31 @@ describe('lifecycles', () => { expect(onBeforeMountFn).toHaveBeenCalled() expect(onBeforeMountFn).toHaveBeenCalled() }) + + it('calls onUnmounted', async () => { + const beforeUnmountFn = jest.fn() + const onBeforeUnmountFn = jest.fn() + const onUnmountFn = jest.fn() + const Component = defineComponent({ + beforeUnmount: beforeUnmountFn, + setup() { + onUnmounted(onUnmountFn) + onBeforeUnmount(onBeforeUnmountFn) + + return () => h('div') + } + }) + + const wrapper = mount(Component) + await nextTick() + expect(beforeUnmountFn).not.toHaveBeenCalled() + expect(onBeforeUnmountFn).not.toHaveBeenCalled() + expect(onUnmountFn).not.toHaveBeenCalled() + + wrapper.unmount() + + expect(beforeUnmountFn).toHaveBeenCalled() + expect(onBeforeUnmountFn).toHaveBeenCalled() + expect(onUnmountFn).toHaveBeenCalled() + }) }) From b7e3561c88a02fc4a545541e99c1716b4691af05 Mon Sep 17 00:00:00 2001 From: pikax Date: Sat, 18 Apr 2020 14:25:08 +0100 Subject: [PATCH 2/3] readme: update `destroy` state with a rename note --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52498f4d1..dea913e9b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ text | ✅ | trigger | ✅ | returns `nextTick`. You can do `await wrapper.find('button').trigger('click')` setProps | ✅ | setData | ❌ | has PR -destroy | ❌ +destroy | ✅ | renamed to `unmount` to match vue3 Lifecycle hook name. props | ❌ contains | ⚰️| use `find` emittedByOrder | ⚰️ | use `emitted` From 3f785331fd2e2ce1a163369c21fcbc34b6ce501d Mon Sep 17 00:00:00 2001 From: pikax Date: Sun, 19 Apr 2020 18:33:09 +0100 Subject: [PATCH 3/3] add tests --- src/error-wrapper.ts | 4 ++++ src/vue-wrapper.ts | 19 +++++++++++++------ tests/findComponent.spec.ts | 5 +++++ tests/lifecycle.spec.ts | 12 +++++++++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/error-wrapper.ts b/src/error-wrapper.ts index d6ff8a4f9..f16687ed0 100644 --- a/src/error-wrapper.ts +++ b/src/error-wrapper.ts @@ -55,4 +55,8 @@ export class ErrorWrapper { trigger() { throw this.wrapperError('trigger') } + + unmount() { + throw this.wrapperError('unmount') + } } diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 29f805f6f..2538db53c 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,4 +1,4 @@ -import { ComponentPublicInstance, nextTick, App } from 'vue' +import { ComponentPublicInstance, nextTick, App, render } from 'vue' import { ShapeFlags } from '@vue/shared' import { DOMWrapper } from './dom-wrapper' @@ -14,11 +14,11 @@ export class VueWrapper implements WrapperAPI { private componentVM: T private rootVM: ComponentPublicInstance - private __app: App + private __app: App | null private __setProps: (props: Record) => void constructor( - app: App, + app: App | null, vm: ComponentPublicInstance, setProps?: (props: Record) => void ) { @@ -99,15 +99,15 @@ export class VueWrapper findComponent(selector: FindComponentSelector): VueWrapper | ErrorWrapper { if (typeof selector === 'object' && 'ref' in selector) { - return createWrapper(this.vm.$refs[selector.ref] as T) + return createWrapper(null, this.vm.$refs[selector.ref] as T) } const result = find(this.vm.$.subTree, selector) if (!result.length) return new ErrorWrapper({ selector }) - return createWrapper(result[0]) + return createWrapper(null, result[0]) } findAllComponents(selector: FindAllComponentsSelector): VueWrapper[] { - return find(this.vm.$.subTree, selector).map((c) => createWrapper(c)) + return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c)) } findAll(selector: string): DOMWrapper[] { @@ -130,6 +130,13 @@ export class VueWrapper } unmount() { + // preventing dispose of child component + if (!this.__app) { + throw new Error( + `wrapper.unmount() can only be called by the root wrapper` + ) + } + if (this.parentElement) { this.parentElement.removeChild(this.element) } diff --git a/tests/findComponent.spec.ts b/tests/findComponent.spec.ts index 499ea1abf..2efe3b774 100644 --- a/tests/findComponent.spec.ts +++ b/tests/findComponent.spec.ts @@ -86,4 +86,9 @@ describe('findComponent', () => { expect(wrapper.findComponent(Hello).text()).toBe('Hello world') expect(wrapper.findComponent(compC).text()).toBe('C') }) + + it('throw error if trying to unmount component from find', () => { + const wrapper = mount(compA) + expect(wrapper.findComponent(Hello).unmount).toThrowError() + }) }) diff --git a/tests/lifecycle.spec.ts b/tests/lifecycle.spec.ts index 3889121f5..442493082 100644 --- a/tests/lifecycle.spec.ts +++ b/tests/lifecycle.spec.ts @@ -5,7 +5,8 @@ import { nextTick, onBeforeMount, onUnmounted, - onBeforeUnmount + onBeforeUnmount, + ref } from 'vue' import { mount } from '../src' @@ -53,10 +54,19 @@ describe('lifecycles', () => { expect(onBeforeUnmountFn).not.toHaveBeenCalled() expect(onUnmountFn).not.toHaveBeenCalled() + const removeChildSpy = jest.spyOn( + wrapper.element.parentElement, + 'removeChild' + ) + + const el = wrapper.element + wrapper.unmount() expect(beforeUnmountFn).toHaveBeenCalled() expect(onBeforeUnmountFn).toHaveBeenCalled() expect(onUnmountFn).toHaveBeenCalled() + + expect(removeChildSpy).toHaveBeenCalledWith(el) }) })