wip: tests for global api compat

This commit is contained in:
Evan You 2021-04-27 17:34:19 -04:00
parent 86703c23a6
commit 1d1af403ca
7 changed files with 466 additions and 67 deletions

View File

@ -1,21 +1,335 @@
import Vue from '@vue/compat'
import { effect, isReactive } from '@vue/reactivity'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning
} from '../compatConfig'
describe('compat: global API', () => {
beforeEach(() => Vue.configureCompat({ MODE: 2 }))
afterEach(() => Vue.configureCompat({ MODE: 3 }))
beforeEach(() => {
Vue.configureCompat({ MODE: 2 })
})
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
describe('GLOBAL_MOUNT', () => {
test('new Vue() with el', () => {
toggleDeprecationWarning(true)
test('should work', () => {
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
el,
compatConfig: { GLOBAL_MOUNT: true },
data() {
return {
msg: 'hello'
}
}
})
expect('global app bootstrapping API has changed').toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(el.innerHTML).toBe('hello')
})
test('new Vue() + $mount', () => {
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
data() {
return {
msg: 'hello'
}
}
}).$mount(el)
expect(el.innerHTML).toBe('hello')
})
})
describe('GLOBAL_MOUNT_CONTAINER', () => {
test('should warn', () => {
toggleDeprecationWarning(true)
const el = document.createElement('div')
el.innerHTML = `test`
el.setAttribute('v-bind:id', 'foo')
new Vue().$mount(el)
// warning only
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT_CONTAINER].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_EXTEND', () => {
// https://github.com/vuejs/vue/blob/dev/test/unit/features/global-api/extend.spec.js
it('should correctly merge options', () => {
toggleDeprecationWarning(true)
const Test = Vue.extend({
name: 'test',
a: 1,
b: 2
})
expect(Test.options.a).toBe(1)
expect(Test.options.b).toBe(2)
expect(Test.super).toBe(Vue)
const t = new Test({
a: 2
})
expect(t.$options.a).toBe(2)
expect(t.$options.b).toBe(2)
// inheritance
const Test2 = Test.extend({
a: 2
})
expect(Test2.options.a).toBe(2)
expect(Test2.options.b).toBe(2)
const t2 = new Test2({
a: 3
})
expect(t2.$options.a).toBe(3)
expect(t2.$options.b).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_EXTEND].message
).toHaveBeenWarned()
})
it('should work when used as components', () => {
const foo = Vue.extend({
template: '<span>foo</span>'
})
const bar = Vue.extend({
template: '<span>bar</span>'
})
const vm = new Vue({
template: '<div><foo></foo><bar></bar></div>',
components: { foo, bar }
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
})
it('should merge lifecycle hooks', () => {
const calls: number[] = []
const A = Vue.extend({
created() {
calls.push(1)
}
})
const B = A.extend({
created() {
calls.push(2)
}
})
new B({
created() {
calls.push(3)
}
})
expect(calls).toEqual([1, 2, 3])
})
it('should not merge nested mixins created with Vue.extend', () => {
const A = Vue.extend({
created: () => {}
})
const B = Vue.extend({
mixins: [A],
created: () => {}
})
const C = Vue.extend({
extends: B,
created: () => {}
})
const D = Vue.extend({
mixins: [C],
created: () => {}
})
expect(D.options.created!.length).toBe(4)
})
it('should merge methods', () => {
const A = Vue.extend({
methods: {
a() {
return this.n
}
}
})
const B = A.extend({
methods: {
b() {
return this.n + 1
}
}
})
const b = new B({
data: () => ({ n: 0 }),
methods: {
c() {
return this.n + 2
}
}
}) as any
expect(b.a()).toBe(0)
expect(b.b()).toBe(1)
expect(b.c()).toBe(2)
})
it('should merge assets', () => {
const A = Vue.extend({
components: {
aa: {
template: '<div>A</div>'
}
}
})
const B = A.extend({
components: {
bb: {
template: '<div>B</div>'
}
}
})
const b = new B({
template: '<div><aa></aa><bb></bb></div>'
}).$mount()
expect(b.$el.innerHTML).toBe('<div>A</div><div>B</div>')
})
it('caching', () => {
const options = {
template: '<div></div>'
}
const A = Vue.extend(options)
const B = Vue.extend(options)
expect(A).toBe(B)
})
it('extended options should use different identify from parent', () => {
const A = Vue.extend({ computed: {} })
const B = A.extend()
B.options.computed.b = () => 'foo'
expect(B.options.computed).not.toBe(A.options.computed)
expect(A.options.computed.b).toBeUndefined()
})
})
describe('GLOBAL_PROTOTYPE', () => {
test('plain properties', () => {
toggleDeprecationWarning(true)
Vue.prototype.$test = 1
const vm = new Vue() as any
expect(vm.$test).toBe(1)
delete Vue.prototype.$test
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_PROTOTYPE].message
).toHaveBeenWarned()
})
test('method this context', () => {
Vue.prototype.$test = function() {
return this.msg
}
const vm = new Vue({
data() {
return { msg: 'method' }
}
}) as any
expect(vm.$test()).toBe('method')
delete Vue.prototype.$test
})
test('defined properties', () => {
Object.defineProperty(Vue.prototype, '$test', {
configurable: true,
get() {
return this.msg
}
})
const vm = new Vue({
data() {
return { msg: 'getter' }
}
}) as any
expect(vm.$test).toBe('getter')
delete Vue.prototype.$test
})
test('extended prototype', async () => {
const Foo = Vue.extend()
Foo.prototype.$test = 1
const vm = new Foo() as any
expect(vm.$test).toBe(1)
const plain = new Vue() as any
expect(plain.$test).toBeUndefined()
})
})
describe('GLOBAL_SET/DELETE', () => {
test('set', () => {
toggleDeprecationWarning(true)
const obj: any = {}
Vue.set(obj, 'foo', 1)
expect(obj.foo).toBe(1)
expect(
deprecationData[DeprecationTypes.GLOBAL_SET].message
).toHaveBeenWarned()
})
test('delete', () => {
toggleDeprecationWarning(true)
const obj: any = { foo: 1 }
Vue.delete(obj, 'foo')
expect('foo' in obj).toBe(false)
expect(
deprecationData[DeprecationTypes.GLOBAL_DELETE].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_OBSERVABLE', () => {
test('should work', () => {
toggleDeprecationWarning(true)
const obj = Vue.observable({})
expect(isReactive(obj)).toBe(true)
expect(
deprecationData[DeprecationTypes.GLOBAL_OBSERVABLE].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_PRIVATE_UTIL', () => {
test('defineReactive', () => {
toggleDeprecationWarning(true)
const obj: any = {}
// @ts-ignore
Vue.util.defineReactive(obj, 'test', 1)
let n
effect(() => {
n = obj.test
})
expect(n).toBe(1)
obj.test++
expect(n).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_PRIVATE_UTIL].message
).toHaveBeenWarned()
})
})

View File

@ -17,7 +17,7 @@ export const enum DeprecationTypes {
GLOBAL_SET = 'GLOBAL_SET',
GLOBAL_DELETE = 'GLOBAL_DELETE',
GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE',
GLOBAL_UTIL = 'GLOBAL_UTIL',
GLOBAL_PRIVATE_UTIL = 'GLOBAL_PRIVATE_UTIL',
CONFIG_SILENT = 'CONFIG_SILENT',
CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS',
@ -70,7 +70,7 @@ type DeprecationData = {
link?: string
}
const deprecationData: Record<DeprecationTypes, DeprecationData> = {
export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
[DeprecationTypes.GLOBAL_MOUNT]: {
message:
`The global app bootstrapping API has changed: vm.$mount() and the "el" ` +
@ -119,7 +119,7 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
link: `https://v3.vuejs.org/api/basic-reactivity.html`
},
[DeprecationTypes.GLOBAL_UTIL]: {
[DeprecationTypes.GLOBAL_PRIVATE_UTIL]: {
message:
`Vue.util has been removed. Please refactor to avoid its usage ` +
`since it was an internal API even in Vue 2.`
@ -437,6 +437,13 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
const instanceWarned: Record<string, true> = Object.create(null)
const warnCount: Record<string, number> = Object.create(null)
// test only
let warningEnabled = true
export function toggleDeprecationWarning(flag: boolean) {
warningEnabled = flag
}
export function warnDeprecation(
key: DeprecationTypes,
instance: ComponentInternalInstance | null,
@ -445,6 +452,9 @@ export function warnDeprecation(
if (!__DEV__) {
return
}
if (__TEST__ && !warningEnabled) {
return
}
instance = instance || getCurrentInstance()
@ -463,14 +473,14 @@ export function warnDeprecation(
// skip if the same warning is emitted for the same component type
const componentDupKey = dupKey + compId
if (componentDupKey in instanceWarned) {
if (!__TEST__ && componentDupKey in instanceWarned) {
return
}
instanceWarned[componentDupKey] = true
// same warning, but different component. skip the long message and just
// log the key and count.
if (dupKey in warnCount) {
if (!__TEST__ && dupKey in warnCount) {
warn(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`)
return
}

View File

@ -1,5 +1,6 @@
import { isPlainObject } from '@vue/shared'
import { isFunction, isPlainObject } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function deepMergeData(
@ -19,3 +20,19 @@ export function deepMergeData(
}
}
}
export function mergeDataOption(to: any, from: any) {
if (!from) {
return to
}
if (!to) {
return from
}
return function mergedDataFn(this: ComponentPublicInstance) {
return deepMergeData(
isFunction(to) ? to.call(this, this) : to,
isFunction(from) ? from.call(this, this) : from,
this.$
)
}
}

View File

@ -25,7 +25,6 @@ import {
CreateAppFunction,
Plugin
} from '../apiCreateApp'
import { defineComponent } from '../apiDefineComponent'
import {
Component,
ComponentOptions,
@ -40,7 +39,7 @@ import { devtoolsInitApp } from '../devtools'
import { Directive } from '../directives'
import { nextTick } from '../scheduler'
import { version } from '..'
import { LegacyConfig } from './globalConfig'
import { LegacyConfig, legacyOptionMergeStrats } from './globalConfig'
import { LegacyDirective } from './customDirective'
import {
warnDeprecation,
@ -50,6 +49,7 @@ import {
isCompatEnabled,
softAssertCompatEnabled
} from './compatConfig'
import { LegacyPublicInstance } from './instance'
/**
* @deprecated the default `Vue` export has been removed in Vue 3. The type for
@ -57,15 +57,17 @@ import {
* named imports instead - e.g. `import { createApp } from 'vue'`.
*/
export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
configureCompat: typeof configureCompat
// no inference here since these types are not meant for actual use - they
// are merely here to provide type checks for internal implementation and
// information for migration.
new (options?: ComponentOptions): ComponentPublicInstance
new (options?: ComponentOptions): LegacyPublicInstance
version: string
config: AppConfig & LegacyConfig
extend: typeof defineComponent
extend: (options?: ComponentOptions) => CompatVue
nextTick: typeof nextTick
use(plugin: Plugin, ...options: any[]): CompatVue
@ -102,8 +104,10 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
* @internal
*/
options: ComponentOptions
configureCompat: typeof configureCompat
/**
* @internal
*/
super: CompatVue
}
export let isCopyingConfig = false
@ -184,32 +188,55 @@ export function createCompatVue(
let cid = 1
Vue.cid = cid
const extendCache = new WeakMap()
function extendCtor(this: any, extendOptions: ComponentOptions = {}) {
assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null)
if (isFunction(extendOptions)) {
extendOptions = extendOptions.options
}
if (extendCache.has(extendOptions)) {
return extendCache.get(extendOptions)
}
const Super = this
function SubVue(inlineOptions?: ComponentOptions) {
if (!inlineOptions) {
return createCompatApp(extendOptions, SubVue)
return createCompatApp(SubVue.options, SubVue)
} else {
return createCompatApp(
{
el: inlineOptions.el,
extends: extendOptions,
mixins: [inlineOptions]
},
mergeOptions(
extend({}, SubVue.options),
inlineOptions,
null,
legacyOptionMergeStrats as any
),
SubVue
)
}
}
SubVue.super = Super
SubVue.prototype = Object.create(Vue.prototype)
SubVue.prototype.constructor = SubVue
// clone non-primitive base option values for edge case of mutating
// extended options
const mergeBase: any = {}
for (const key in Super.options) {
const superValue = Super.options[key]
mergeBase[key] = isArray(superValue)
? superValue.slice()
: isObject(superValue)
? extend(Object.create(null), superValue)
: superValue
}
SubVue.options = mergeOptions(
extend({}, Super.options) as ComponentOptions,
extendOptions
mergeBase,
extendOptions,
null,
legacyOptionMergeStrats as any
)
SubVue.options._base = SubVue
@ -217,6 +244,8 @@ export function createCompatVue(
SubVue.mixin = Super.mixin
SubVue.use = Super.use
SubVue.cid = ++cid
extendCache.set(extendOptions, SubVue)
return SubVue
}
@ -279,12 +308,17 @@ export function createCompatVue(
warn: __DEV__ ? warn : NOOP,
extend,
mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) =>
mergeOptions(parent, child, vm && vm.$),
mergeOptions(
parent,
child,
vm && vm.$,
vm ? undefined : (legacyOptionMergeStrats as any)
),
defineReactive
}
Object.defineProperty(Vue, 'util', {
get() {
assertCompatEnabled(DeprecationTypes.GLOBAL_UTIL, null)
assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null)
return util
}
})
@ -332,7 +366,7 @@ export function installCompatMount(
// Note: the following assumes DOM environment since the compat build
// only targets web. It essentially includes logic for app.mount from
// both runtime-core AND runtime-dom.
instance.ctx._compat_mount = (selectorOrEl: string | Element) => {
instance.ctx._compat_mount = (selectorOrEl?: string | Element) => {
if (isMounted) {
__DEV__ && warn(`Root instance is already mounted.`)
return
@ -351,14 +385,8 @@ export function installCompatMount(
}
container = result
} else {
if (!selectorOrEl) {
__DEV__ &&
warn(
`Failed to mount root instance: invalid mount target ${selectorOrEl}.`
)
return
}
container = selectorOrEl
// eslint-disable-next-line
container = selectorOrEl || document.createElement('div')
}
const isSVG = container instanceof SVGElement

View File

@ -1,7 +1,7 @@
import { extend, isArray, isString } from '@vue/shared'
import { AppConfig } from '../apiCreateApp'
import { isRuntimeOnly } from '../component'
import { deepMergeData } from './data'
import { mergeDataOption } from './data'
import {
DeprecationTypes,
warnDeprecation,
@ -80,34 +80,36 @@ export function installLegacyConfigProperties(config: AppConfig) {
// Internal merge strats which are no longer needed in v3, but we need to
// expose them because some v2 plugins will reuse these internal strats to
// merge their custom options.
const strats = config.optionMergeStrategies as any
strats.data = deepMergeData
// lifecycle hooks
strats.beforeCreate = mergeHook
strats.created = mergeHook
strats.beforeMount = mergeHook
strats.mounted = mergeHook
strats.beforeUpdate = mergeHook
strats.updated = mergeHook
strats.beforeDestroy = mergeHook
strats.destroyed = mergeHook
strats.activated = mergeHook
strats.deactivated = mergeHook
strats.errorCaptured = mergeHook
strats.serverPrefetch = mergeHook
extend(config.optionMergeStrategies, legacyOptionMergeStrats)
}
export const legacyOptionMergeStrats = {
data: mergeDataOption,
beforeCreate: mergeHook,
created: mergeHook,
beforeMount: mergeHook,
mounted: mergeHook,
beforeUpdate: mergeHook,
updated: mergeHook,
beforeDestroy: mergeHook,
destroyed: mergeHook,
activated: mergeHook,
deactivated: mergeHook,
errorCaptured: mergeHook,
serverPrefetch: mergeHook,
// assets
strats.components = mergeObjectOptions
strats.directives = mergeObjectOptions
strats.filters = mergeObjectOptions
components: mergeObjectOptions,
directives: mergeObjectOptions,
filters: mergeObjectOptions,
// objects
strats.props = mergeObjectOptions
strats.methods = mergeObjectOptions
strats.inject = mergeObjectOptions
strats.computed = mergeObjectOptions
props: mergeObjectOptions,
methods: mergeObjectOptions,
inject: mergeObjectOptions,
computed: mergeObjectOptions,
// watch has special merge behavior in v2, but isn't actually needed in v3.
// since we are only exposing these for compat and nobody should be relying
// on the watch-specific behavior, just expose the object merge strat.
strats.watch = mergeObjectOptions
watch: mergeObjectOptions
}
function mergeHook(

View File

@ -6,7 +6,10 @@ import {
toDisplayString,
toNumber
} from '@vue/shared'
import { PublicPropertiesMap } from '../componentPublicInstance'
import {
ComponentPublicInstance,
PublicPropertiesMap
} from '../componentPublicInstance'
import { getCompatChildren } from './instanceChildren'
import {
DeprecationTypes,
@ -33,6 +36,23 @@ import {
} from './renderHelpers'
import { resolveFilter } from '../helpers/resolveAssets'
import { resolveMergedOptions } from '../componentOptions'
import { Slots } from '../componentSlots'
export type LegacyPublicInstance = ComponentPublicInstance &
LegacyPublicProperties
export interface LegacyPublicProperties {
$set(target: object, key: string, value: any): void
$delete(target: object, key: string): void
$mount(el?: string | Element): this
$destroy(): void
$scopedSlots: Slots
$on(event: string | string[], fn: Function): this
$once(event: string, fn: Function): this
$off(event?: string, fn?: Function): this
$children: LegacyPublicProperties[]
$listeners: Record<string, Function | Function[]>
}
export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const set = (target: any, key: any, val: any) => {

View File

@ -893,7 +893,13 @@ function callHookWithMixinAndExtends(
}
}
if (selfHook) {
callWithAsyncErrorHandling(selfHook.bind(instance.proxy!), instance, type)
callWithAsyncErrorHandling(
__COMPAT__ && isArray(selfHook)
? selfHook.map(h => h.bind(instance.proxy!))
: selfHook.bind(instance.proxy!),
instance,
type
)
}
}
@ -1007,21 +1013,23 @@ export function resolveMergedOptions(
export function mergeOptions(
to: any,
from: any,
instance?: ComponentInternalInstance
instance?: ComponentInternalInstance | null,
strats = instance && instance.appContext.config.optionMergeStrategies
) {
if (__COMPAT__ && isFunction(from)) {
from = from.options
}
const strats = instance && instance.appContext.config.optionMergeStrategies
const { mixins, extends: extendsOptions } = from
extendsOptions && mergeOptions(to, extendsOptions, instance)
extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
mixins &&
mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance))
mixins.forEach((m: ComponentOptionsMixin) =>
mergeOptions(to, m, instance, strats)
)
for (const key in from) {
if (strats && hasOwn(to, key) && hasOwn(strats, key)) {
if (strats && hasOwn(strats, key)) {
to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
} else {
to[key] = from[key]