diff --git a/packages/runtime-core/src/compat/__tests__/instance.spec.ts b/packages/runtime-core/src/compat/__tests__/instance.spec.ts new file mode 100644 index 000000000..bddc77e25 --- /dev/null +++ b/packages/runtime-core/src/compat/__tests__/instance.spec.ts @@ -0,0 +1,299 @@ +import Vue from '@vue/compat' +import { Slots } from '../../componentSlots' +import { Text } from '../../vnode' +import { + DeprecationTypes, + deprecationData, + toggleDeprecationWarning +} from '../compatConfig' +import { LegacyPublicInstance } from '../instance' + +beforeEach(() => { + toggleDeprecationWarning(true) + Vue.configureCompat({ + MODE: 2, + GLOBAL_MOUNT: 'suppress-warning' + }) +}) + +afterEach(() => { + toggleDeprecationWarning(false) + Vue.configureCompat({ MODE: 3 }) +}) + +test('INSTANCE_SET', () => { + const obj: any = {} + new Vue().$set(obj, 'foo', 1) + expect(obj.foo).toBe(1) + expect( + deprecationData[DeprecationTypes.INSTANCE_SET].message + ).toHaveBeenWarned() +}) + +test('INSTANCE_DELETE', () => { + const obj: any = { foo: 1 } + new Vue().$delete(obj, 'foo') + expect('foo' in obj).toBe(false) + expect( + deprecationData[DeprecationTypes.INSTANCE_DELETE].message + ).toHaveBeenWarned() +}) + +test('INSTANCE_DESTROY', () => { + new Vue({ template: 'foo' }).$mount().$destroy() + expect( + deprecationData[DeprecationTypes.INSTANCE_DESTROY].message + ).toHaveBeenWarned() +}) + +// https://github.com/vuejs/vue/blob/dev/test/unit/features/instance/methods-events.spec.js +describe('INSTANCE_EVENT_EMITTER', () => { + let vm: LegacyPublicInstance + let spy: jest.Mock + + beforeEach(() => { + vm = new Vue() + spy = jest.fn() + }) + + it('$on', () => { + vm.$on('test', function(this: any) { + // expect correct context + expect(this).toBe(vm) + spy.apply(this, arguments) + }) + vm.$emit('test', 1, 2, 3, 4) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(1, 2, 3, 4) + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$on multi event', () => { + vm.$on(['test1', 'test2'], function(this: any) { + expect(this).toBe(vm) + spy.apply(this, arguments) + }) + vm.$emit('test1', 1, 2, 3, 4) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(1, 2, 3, 4) + vm.$emit('test2', 5, 6, 7, 8) + expect(spy).toHaveBeenCalledTimes(2) + expect(spy).toHaveBeenCalledWith(5, 6, 7, 8) + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$off multi event', () => { + vm.$on(['test1', 'test2', 'test3'], spy) + vm.$off(['test1', 'test2'], spy) + vm.$emit('test1') + vm.$emit('test2') + expect(spy).not.toHaveBeenCalled() + vm.$emit('test3', 1, 2, 3, 4) + expect(spy).toHaveBeenCalledTimes(1) + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$off multi event without callback', () => { + vm.$on(['test1', 'test2'], spy) + vm.$off(['test1', 'test2']) + vm.$emit('test1') + expect(spy).not.toHaveBeenCalled() + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$once', () => { + vm.$once('test', spy) + vm.$emit('test', 1, 2, 3) + vm.$emit('test', 2, 3, 4) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(1, 2, 3) + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$off event added by $once', () => { + vm.$once('test', spy) + vm.$off('test', spy) // test off event and this event added by once + vm.$emit('test', 1, 2, 3) + expect(spy).not.toHaveBeenCalled() + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$off', () => { + vm.$on('test1', spy) + vm.$on('test2', spy) + vm.$off() + vm.$emit('test1') + vm.$emit('test2') + expect(spy).not.toHaveBeenCalled() + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$off event', () => { + vm.$on('test1', spy) + vm.$on('test2', spy) + vm.$off('test1') + vm.$off('test1') // test off something that's already off + vm.$emit('test1', 1) + vm.$emit('test2', 2) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(2) + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) + + it('$off event + fn', () => { + const spy2 = jasmine.createSpy('emitter') + vm.$on('test', spy) + vm.$on('test', spy2) + vm.$off('test', spy) + vm.$emit('test', 1, 2, 3) + expect(spy).not.toHaveBeenCalled() + expect(spy2).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledWith(1, 2, 3) + expect( + deprecationData[DeprecationTypes.INSTANCE_EVENT_EMITTER].message + ).toHaveBeenWarned() + }) +}) + +describe('INSTANCE_EVENT_HOOKS', () => { + test('instance API', () => { + const spy = jest.fn() + const vm = new Vue({ template: 'foo' }) + vm.$on('hook:mounted', spy) + vm.$mount() + expect(spy).toHaveBeenCalled() + expect( + (deprecationData[DeprecationTypes.INSTANCE_EVENT_HOOKS] + .message as Function)('hook:mounted') + ).toHaveBeenWarned() + }) + + test('via template', () => { + const spy = jest.fn() + new Vue({ + template: ``, + methods: { spy }, + components: { + child: { + template: 'foo' + } + } + }).$mount() + expect(spy).toHaveBeenCalled() + expect( + (deprecationData[DeprecationTypes.INSTANCE_EVENT_HOOKS] + .message as Function)('hook:mounted') + ).toHaveBeenWarned() + }) +}) + +test('INSTANCE_EVENT_CHILDREN', () => { + const vm = new Vue({ + template: `
`, + components: { + child: { + template: 'foo', + data() { + return { n: 1 } + } + } + } + }).$mount() + expect(vm.$children.length).toBe(4) + vm.$children.forEach((c: any) => { + expect(c.n).toBe(1) + }) + expect( + deprecationData[DeprecationTypes.INSTANCE_CHILDREN].message + ).toHaveBeenWarned() +}) + +test('INSTANCE_LISTENERS', () => { + const foo = () => 'foo' + const bar = () => 'bar' + let listeners: Record + + new Vue({ + template: ``, + methods: { foo, bar }, + components: { + child: { + template: `
`, + mounted() { + listeners = this.$listeners + } + } + } + }).$mount() + + expect(Object.keys(listeners!)).toMatchObject(['click', 'custom']) + expect(listeners!.click()).toBe('foo') + expect(listeners!.custom()).toBe('bar') + + expect( + deprecationData[DeprecationTypes.INSTANCE_LISTENERS].message + ).toHaveBeenWarned() +}) + +test('INSTANCE_SCOPED_SLOTS', () => { + let slots: Slots + new Vue({ + template: `{{ msg }}`, + components: { + child: { + compatConfig: { RENDER_FUNCTION: false }, + render() { + slots = this.$scopedSlots + } + } + } + }).$mount() + + expect(slots!.default!({ msg: 'hi' })).toMatchObject([ + { + type: Text, + children: 'hi' + } + ]) + + expect( + deprecationData[DeprecationTypes.INSTANCE_SCOPED_SLOTS].message + ).toHaveBeenWarned() +}) + +test('INSTANCE_ATTR_CLASS_STYLE', () => { + const vm = new Vue({ + template: ``, + components: { + child: { + inheritAttrs: false, + template: `
` + } + } + }).$mount() + + expect(vm.$el.outerHTML).toBe( + `
` + ) + + expect( + (deprecationData[DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE] + .message as Function)('Anonymous') + ).toHaveBeenWarned() +}) diff --git a/packages/runtime-core/src/compat/__tests__/options.spec.ts b/packages/runtime-core/src/compat/__tests__/options.spec.ts new file mode 100644 index 000000000..a10d35770 --- /dev/null +++ b/packages/runtime-core/src/compat/__tests__/options.spec.ts @@ -0,0 +1,92 @@ +import Vue from '@vue/compat' +import { nextTick } from '../../scheduler' +import { + DeprecationTypes, + deprecationData, + toggleDeprecationWarning +} from '../compatConfig' + +beforeEach(() => { + toggleDeprecationWarning(true) + Vue.configureCompat({ + MODE: 2, + GLOBAL_MOUNT: 'suppress-warning' + }) +}) + +afterEach(() => { + toggleDeprecationWarning(false) + Vue.configureCompat({ MODE: 3 }) +}) + +test('root data plain object', () => { + const vm = new Vue({ + data: { foo: 1 } as any, + template: `{{ foo }}` + }).$mount() + expect(vm.$el.textContent).toBe('1') + expect( + deprecationData[DeprecationTypes.OPTIONS_DATA_FN].message + ).toHaveBeenWarned() +}) + +test('data deep merge', () => { + const mixin = { + data() { + return { + foo: { + baz: 2 + } + } + } + } + + const vm = new Vue({ + mixins: [mixin], + data: () => ({ + foo: { + bar: 1 + } + }), + template: `{{ foo }}` + }).$mount() + + expect(vm.$el.textContent).toBe(JSON.stringify({ baz: 2, bar: 1 }, null, 2)) + expect( + (deprecationData[DeprecationTypes.OPTIONS_DATA_MERGE].message as Function)( + 'foo' + ) + ).toHaveBeenWarned() +}) + +test('beforeDestroy/destroyed', async () => { + const beforeDestroy = jest.fn() + const destroyed = jest.fn() + + const child = { + template: `foo`, + beforeDestroy, + destroyed + } + + const vm = new Vue({ + template: ``, + data() { + return { ok: true } + }, + components: { child } + }).$mount() as any + + vm.ok = false + await nextTick() + expect(beforeDestroy).toHaveBeenCalled() + expect(destroyed).toHaveBeenCalled() + + expect( + deprecationData[DeprecationTypes.OPTIONS_BEFORE_DESTROY].message + ).toHaveBeenWarned() + + expect( + deprecationData[DeprecationTypes.OPTIONS_DESTROYED].message + ).toHaveBeenWarned() +}) diff --git a/packages/runtime-core/src/compat/compatConfig.ts b/packages/runtime-core/src/compat/compatConfig.ts index e59bdb0a2..d715d9704 100644 --- a/packages/runtime-core/src/compat/compatConfig.ts +++ b/packages/runtime-core/src/compat/compatConfig.ts @@ -230,7 +230,8 @@ export const deprecationData: Record = { [DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE]: { message: componentName => - `Component <${componentName}> has \`inheritAttrs: false\` but is ` + + `Component <${componentName || + 'Anonymous'}> has \`inheritAttrs: false\` but is ` + `relying on class/style fallthrough from parent. In Vue 3, class/style ` + `are now included in $attrs and will no longer fallthrough when ` + `inheritAttrs is false. If you are already using v-bind="$attrs" on ` + diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts index 5dba5f4b1..dbb521732 100644 --- a/packages/runtime-core/src/compat/global.ts +++ b/packages/runtime-core/src/compat/global.ts @@ -36,7 +36,7 @@ import { } from '../component' import { RenderFunction, mergeOptions } from '../componentOptions' import { ComponentPublicInstance } from '../componentPublicInstance' -import { devtoolsInitApp } from '../devtools' +import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools' import { Directive } from '../directives' import { nextTick } from '../scheduler' import { version } from '..' @@ -456,7 +456,17 @@ export function installCompatMount( return instance.proxy! } - instance.ctx._compat_destroy = app.unmount + instance.ctx._compat_destroy = () => { + if (isMounted) { + render(null, app._container) + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsUnmountApp(app) + } + delete app._container.__vue_app__ + } else if (__DEV__) { + warn(`Cannot unmount an app that is not mounted.`) + } + } return instance.proxy! } diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index 10c59cebb..46e3ef631 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -49,7 +49,7 @@ export interface LegacyPublicProperties { $scopedSlots: Slots $on(event: string | string[], fn: Function): this $once(event: string, fn: Function): this - $off(event?: string, fn?: Function): this + $off(event?: string | string[], fn?: Function): this $children: LegacyPublicProperties[] $listeners: Record } diff --git a/packages/runtime-core/src/compat/instanceEventEmitter.ts b/packages/runtime-core/src/compat/instanceEventEmitter.ts index 64237e916..b314f686c 100644 --- a/packages/runtime-core/src/compat/instanceEventEmitter.ts +++ b/packages/runtime-core/src/compat/instanceEventEmitter.ts @@ -61,13 +61,13 @@ export function once( export function off( instance: ComponentInternalInstance, - event?: string, + event?: string | string[], fn?: Function ) { assertCompatEnabled(DeprecationTypes.INSTANCE_EVENT_EMITTER, instance) const vm = instance.proxy // all - if (!arguments.length) { + if (!event) { eventRegistryMap.set(instance, Object.create(null)) return vm } @@ -93,12 +93,12 @@ export function off( export function emit( instance: ComponentInternalInstance, event: string, - ...args: any[] + args: any[] ) { const cbs = getRegistry(instance)[event] if (cbs) { callWithAsyncErrorHandling( - cbs, + cbs.map(cb => cb.bind(instance.proxy)), instance, ErrorCodes.COMPONENT_EVENT_HANDLER, args diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 4555b9fbc..c075b3e5a 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -446,9 +446,6 @@ function baseCreateRenderer( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { - const isHookEventCompatEnabled = - __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, null) - // compile-time feature flags check if (__ESM_BUNDLER__ && !__TEST__) { initFeatureFlags() @@ -1426,7 +1423,10 @@ function baseCreateRenderer( if ((vnodeHook = props && props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parent, initialVNode) } - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { instance.emit('hook:beforeMount') } @@ -1484,7 +1484,10 @@ function baseCreateRenderer( parentSuspense ) } - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { queuePostRenderEffect( () => instance.emit('hook:mounted'), parentSuspense @@ -1496,7 +1499,10 @@ function baseCreateRenderer( // since the hook may be injected by a child keep-alive if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { instance.a && queuePostRenderEffect(instance.a, parentSuspense) - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { queuePostRenderEffect( () => instance.emit('hook:activated'), parentSuspense @@ -1537,7 +1543,10 @@ function baseCreateRenderer( if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) { invokeVNodeHook(vnodeHook, parent, next, vnode) } - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { instance.emit('hook:beforeUpdate') } @@ -1587,7 +1596,10 @@ function baseCreateRenderer( parentSuspense ) } - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { queuePostRenderEffect( () => instance.emit('hook:updated'), parentSuspense @@ -2253,7 +2265,10 @@ function baseCreateRenderer( if (bum) { invokeArrayFns(bum) } - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { instance.emit('hook:beforeDestroy') } @@ -2272,7 +2287,10 @@ function baseCreateRenderer( if (um) { queuePostRenderEffect(um, parentSuspense) } - if (__COMPAT__ && isHookEventCompatEnabled) { + if ( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) + ) { queuePostRenderEffect( () => instance.emit('hook:destroyed'), parentSuspense