diff --git a/README.md b/README.md index 0dd74f552..d68441e8c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,8 @@ trigger | ✅ | returns `nextTick`. You can do `await wrapper.find('button').tri setProps | ✅ | props | ✅ setData | ❌ | has PR -destroy | ❌ +destroy | ✅ | renamed to `unmount` to match Vue 3 lifecycle hook name. +props | ❌ contains | ⚰️| use `find` emittedByOrder | ⚰️ | use `emitted` setSelected | ⚰️ | now part of `setValue` 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/mount.ts b/src/mount.ts index a125b86d4..3e06a2f78 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -105,11 +105,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) { @@ -121,39 +121,39 @@ 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 - vm.mixin(attachEmitListener()) + app.mixin(attachEmitListener()) // stubs if (options?.global?.stubs) { @@ -163,7 +163,8 @@ export function mount( } // mount the app! - const app = vm.mount(el) - const App = app.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance - return createWrapper(App, setProps) + const vm = app.mount(el) + + const App = vm.$refs[MOUNT_COMPONENT_REF] as ComponentPublicInstance + return createWrapper(app, App, setProps) } diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 39748a00e..2538db53c 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,4 +1,4 @@ -import { ComponentPublicInstance, nextTick } from 'vue' +import { ComponentPublicInstance, nextTick, App, render } from 'vue' import { ShapeFlags } from '@vue/shared' import { DOMWrapper } from './dom-wrapper' @@ -14,12 +14,15 @@ export class VueWrapper implements WrapperAPI { private componentVM: T private rootVM: ComponentPublicInstance + private __app: App | null private __setProps: (props: Record) => void constructor( + app: App | null, vm: ComponentPublicInstance, setProps?: (props: Record) => void ) { + this.__app = app this.rootVM = vm.$root this.componentVM = vm as T this.__setProps = setProps @@ -96,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[] { @@ -125,11 +128,26 @@ export class VueWrapper const rootElementWrapper = new DOMWrapper(this.element) return rootElementWrapper.trigger(eventString) } + + 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) + } + this.__app.unmount(this.element) + } } export function createWrapper( + app: App, vm: ComponentPublicInstance, setProps?: (props: Record) => void ): VueWrapper { - return new VueWrapper(vm, setProps) + return new VueWrapper(app, vm, setProps) } 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 97a116850..442493082 100644 --- a/tests/lifecycle.spec.ts +++ b/tests/lifecycle.spec.ts @@ -1,4 +1,13 @@ -import { defineComponent, h, onMounted, nextTick, onBeforeMount } from 'vue' +import { + defineComponent, + h, + onMounted, + nextTick, + onBeforeMount, + onUnmounted, + onBeforeUnmount, + ref +} from 'vue' import { mount } from '../src' @@ -24,4 +33,40 @@ 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() + + 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) + }) })