diff --git a/packages/runtime-core/src/compat/__tests__/misc.spec.ts b/packages/runtime-core/src/compat/__tests__/misc.spec.ts new file mode 100644 index 000000000..21574ca3f --- /dev/null +++ b/packages/runtime-core/src/compat/__tests__/misc.spec.ts @@ -0,0 +1,249 @@ +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 }) +}) + +function triggerEvent( + target: Element, + event: string, + process?: (e: any) => any +) { + const e = document.createEvent('HTMLEvents') + e.initEvent(event, true, true) + if (process) process(e) + target.dispatchEvent(e) + return e +} + +test('WATCH_ARRAY', async () => { + const spy = jest.fn() + const vm = new Vue({ + data() { + return { + foo: [] + } + }, + watch: { + foo: spy + } + }) as any + expect( + deprecationData[DeprecationTypes.WATCH_ARRAY].message + ).toHaveBeenWarned() + + expect(spy).not.toHaveBeenCalled() + vm.foo.push(1) + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) +}) + +test('PROPS_DEFAULT_THIS', () => { + let thisCtx: any + const Child = { + customOption: 1, + inject: ['provided'], + props: { + foo: null, + bar: { + default(this: any) { + // copy values since injection must be sync + thisCtx = { + foo: this.foo, + $options: this.$options, + provided: this.provided + } + return this.foo + 1 + } + } + }, + template: `{{ bar }}` + } + + const vm = new Vue({ + components: { Child }, + provide: { + provided: 2 + }, + template: `` + }).$mount() + + expect(vm.$el.textContent).toBe('1') + // other props + expect(thisCtx.foo).toBe(0) + // $options + expect(thisCtx.$options.customOption).toBe(1) + // injections + expect(thisCtx.provided).toBe(2) + + expect( + (deprecationData[DeprecationTypes.PROPS_DEFAULT_THIS].message as Function)( + 'bar' + ) + ).toHaveBeenWarned() +}) + +test('V_FOR_REF', async () => { + const vm = new Vue({ + data() { + return { + ok: true, + list: [1, 2, 3] + } + }, + template: ` + + ` + }).$mount() as any + + const mapRefs = () => vm.$refs.list.map((el: HTMLElement) => el.textContent) + expect(mapRefs()).toMatchObject(['1', '2', '3']) + + expect(deprecationData[DeprecationTypes.V_FOR_REF].message).toHaveBeenWarned() + + vm.list.push(4) + await nextTick() + expect(mapRefs()).toMatchObject(['1', '2', '3', '4']) + + vm.list.shift() + await nextTick() + expect(mapRefs()).toMatchObject(['2', '3', '4']) + + vm.ok = !vm.ok + await nextTick() + expect(mapRefs()).toMatchObject([]) + + vm.ok = !vm.ok + await nextTick() + expect(mapRefs()).toMatchObject(['2', '3', '4']) +}) + +test('V_ON_KEYCODE_MODIFIER', () => { + const spy = jest.fn() + const vm = new Vue({ + template: ``, + methods: { spy } + }).$mount() + triggerEvent(vm.$el, 'keyup', e => { + e.key = '_' + e.keyCode = 1 + }) + expect(spy).toHaveBeenCalled() + expect( + deprecationData[DeprecationTypes.V_ON_KEYCODE_MODIFIER].message + ).toHaveBeenWarned() +}) + +test('CUSTOM_DIR', async () => { + const myDir = { + bind: jest.fn(), + inserted: jest.fn(), + update: jest.fn(), + componentUpdated: jest.fn(), + unbind: jest.fn() + } as any + + const getCalls = () => + Object.keys(myDir).map(key => myDir[key].mock.calls.length) + + const vm = new Vue({ + data() { + return { + ok: true, + foo: 1 + } + }, + template: `
`, + directives: { + myDir + } + }).$mount() as any + + expect(getCalls()).toMatchObject([1, 1, 0, 0, 0]) + + expect( + (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)( + 'bind', + 'beforeMount' + ) + ).toHaveBeenWarned() + expect( + (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)( + 'inserted', + 'mounted' + ) + ).toHaveBeenWarned() + + vm.foo++ + await nextTick() + expect(getCalls()).toMatchObject([1, 1, 1, 1, 0]) + + expect( + (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)( + 'update', + 'updated' + ) + ).toHaveBeenWarned() + expect( + (deprecationData[DeprecationTypes.CUSTOM_DIR].message as Function)( + 'componentUpdated', + 'updated' + ) + ).toHaveBeenWarned() +}) + +test('ATTR_FALSE_VALUE', () => { + const vm = new Vue({ + template: `
` + }).$mount() + expect(vm.$el.hasAttribute('id')).toBe(false) + expect(vm.$el.hasAttribute('foo')).toBe(false) + expect( + (deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)( + 'id' + ) + ).toHaveBeenWarned() + expect( + (deprecationData[DeprecationTypes.ATTR_FALSE_VALUE].message as Function)( + 'foo' + ) + ).toHaveBeenWarned() +}) + +test('ATTR_ENUMERATED_COERSION', () => { + const vm = new Vue({ + template: `
` + }).$mount() + expect(vm.$el.getAttribute('draggable')).toBe('false') + expect(vm.$el.getAttribute('spellcheck')).toBe('true') + expect(vm.$el.getAttribute('contenteditable')).toBe('true') + expect( + (deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERSION] + .message as Function)('draggable', null, 'false') + ).toHaveBeenWarned() + expect( + (deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERSION] + .message as Function)('spellcheck', 0, 'true') + ).toHaveBeenWarned() + expect( + (deprecationData[DeprecationTypes.ATTR_ENUMERATED_COERSION] + .message as Function)('contenteditable', 'foo', 'true') + ).toHaveBeenWarned() +}) diff --git a/packages/runtime-core/src/compat/customDirective.ts b/packages/runtime-core/src/compat/customDirective.ts index dc854dbca..da351eb08 100644 --- a/packages/runtime-core/src/compat/customDirective.ts +++ b/packages/runtime-core/src/compat/customDirective.ts @@ -32,13 +32,13 @@ export function mapCompatDirectiveHook( if (mappedName) { if (isArray(mappedName)) { const hook: DirectiveHook[] = [] - mappedName.forEach(name => { - const mappedHook = dir[name] + mappedName.forEach(mapped => { + const mappedHook = dir[mapped] if (mappedHook) { softAssertCompatEnabled( DeprecationTypes.CUSTOM_DIR, instance, - mappedName, + mapped, name ) hook.push(mappedHook) diff --git a/packages/runtime-dom/src/modules/attrs.ts b/packages/runtime-dom/src/modules/attrs.ts index fc3ead526..4e1621e92 100644 --- a/packages/runtime-dom/src/modules/attrs.ts +++ b/packages/runtime-dom/src/modules/attrs.ts @@ -1,4 +1,9 @@ -import { isSpecialBooleanAttr } from '@vue/shared' +import { isSpecialBooleanAttr, makeMap, NOOP } from '@vue/shared' +import { + compatUtils, + ComponentInternalInstance, + DeprecationTypes +} from '@vue/runtime-core' export const xlinkNS = 'http://www.w3.org/1999/xlink' @@ -6,7 +11,8 @@ export function patchAttr( el: Element, key: string, value: any, - isSVG: boolean + isSVG: boolean, + instance?: ComponentInternalInstance | null ) { if (isSVG && key.startsWith('xlink:')) { if (value == null) { @@ -15,7 +21,7 @@ export function patchAttr( el.setAttributeNS(xlinkNS, key, value) } } else { - if (__COMPAT__ && compatCoerceAttr(el, key, value)) { + if (__COMPAT__ && compatCoerceAttr(el, key, value, instance)) { return } @@ -31,9 +37,6 @@ export function patchAttr( } // 2.x compat -import { makeMap, NOOP } from '@vue/shared' -import { compatUtils, DeprecationTypes } from '@vue/runtime-core' - const isEnumeratedAttr = __COMPAT__ ? /*#__PURE__*/ makeMap('contenteditable,draggable,spellcheck') : NOOP @@ -41,7 +44,8 @@ const isEnumeratedAttr = __COMPAT__ export function compatCoerceAttr( el: Element, key: string, - value: unknown + value: unknown, + instance: ComponentInternalInstance | null = null ): boolean { if (isEnumeratedAttr(key)) { const v2CocercedValue = @@ -54,7 +58,7 @@ export function compatCoerceAttr( v2CocercedValue && compatUtils.softAssertCompatEnabled( DeprecationTypes.ATTR_ENUMERATED_COERSION, - null, + instance, key, value, v2CocercedValue @@ -68,7 +72,7 @@ export function compatCoerceAttr( !isSpecialBooleanAttr(key) && compatUtils.softAssertCompatEnabled( DeprecationTypes.ATTR_FALSE_VALUE, - null, + instance, key ) ) { diff --git a/packages/runtime-dom/src/modules/props.ts b/packages/runtime-dom/src/modules/props.ts index 068701642..01a16a58c 100644 --- a/packages/runtime-dom/src/modules/props.ts +++ b/packages/runtime-dom/src/modules/props.ts @@ -2,7 +2,7 @@ // Reason: potentially setting innerHTML. // This can come from explicit usage of v-html or innerHTML as a prop in render -import { warn } from '@vue/runtime-core' +import { warn, DeprecationTypes, compatUtils } from '@vue/runtime-core' // functions. The user is responsible for using them with only trusted content. export function patchDOMProp( @@ -55,6 +55,28 @@ export function patchDOMProp( } } + if ( + __COMPAT__ && + value === false && + compatUtils.isCompatEnabled( + DeprecationTypes.ATTR_FALSE_VALUE, + parentComponent + ) + ) { + const type = typeof el[key] + if (type === 'string' || type === 'number') { + __DEV__ && + compatUtils.warnDeprecation( + DeprecationTypes.ATTR_FALSE_VALUE, + parentComponent, + key + ) + el[key] = type === 'number' ? 0 : '' + el.removeAttribute(key) + return + } + } + // some properties perform value validation and throw try { el[key] = value diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index b7ae61e21..2754f7426 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -58,7 +58,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = ( } else if (key === 'false-value') { ;(el as any)._falseValue = nextValue } - patchAttr(el, key, nextValue, isSVG) + patchAttr(el, key, nextValue, isSVG, parentComponent) } break }