Merge remote-tracking branch 'github/master' into changing_unwrap_ref

This commit is contained in:
pikax
2020-04-13 18:32:14 +01:00
54 changed files with 1175 additions and 2676 deletions

View File

@@ -1,146 +0,0 @@
import {
h,
ref,
render,
nodeOps,
nextTick,
defineComponent
} from '@vue/runtime-test'
describe('renderer: component', () => {
test.todo('should work')
test.todo('shouldUpdateComponent')
test.todo('componentProxy')
describe('componentProps', () => {
test.todo('should work')
test('should convert empty booleans to true', () => {
let b1: any, b2: any, b3: any
const Comp = defineComponent({
props: {
b1: Boolean,
b2: [Boolean, String],
b3: [String, Boolean]
},
setup(props) {
;({ b1, b2, b3 } = props)
return () => ''
}
})
render(
h(Comp, <any>{ b1: '', b2: '', b3: '' }),
nodeOps.createElement('div')
)
expect(b1).toBe(true)
expect(b2).toBe(true)
expect(b3).toBe('')
})
})
describe('slots', () => {
test('should respect $stable flag', async () => {
const flag1 = ref(1)
const flag2 = ref(2)
const spy = jest.fn()
const Child = () => {
spy()
return 'child'
}
const App = {
setup() {
return () => [
flag1.value,
h(
Child,
{ n: flag2.value },
{
foo: () => 'foo',
$stable: true
}
)
]
}
}
render(h(App), nodeOps.createElement('div'))
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props didn't change, slots are stable
// -> child should not update
flag1.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props changed
// -> child should update
flag2.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
})
})
test('emit', async () => {
let noMatchEmitResult: any
let singleEmitResult: any
let multiEmitResult: any
const Child = defineComponent({
setup(_, { emit }) {
noMatchEmitResult = emit('foo')
singleEmitResult = emit('bar')
multiEmitResult = emit('baz')
return () => h('div')
}
})
const App = {
setup() {
return () =>
h(Child, {
// emit triggering single handler
onBar: () => 1,
// emit triggering multiple handlers
onBaz: [() => Promise.resolve(2), () => Promise.resolve(3)]
})
}
}
render(h(App), nodeOps.createElement('div'))
// assert return values from emit
expect(noMatchEmitResult).toMatchObject([])
expect(singleEmitResult).toMatchObject([1])
expect(await Promise.all(multiEmitResult)).toMatchObject([2, 3])
})
// for v-model:foo-bar usage in DOM templates
test('emit update:xxx events should trigger kebab-case equivalent', () => {
const Child = defineComponent({
setup(_, { emit }) {
emit('update:fooBar', 1)
return () => h('div')
}
})
const handler = jest.fn()
const App = {
setup() {
return () =>
h(Child, {
'onUpdate:foo-bar': handler
})
}
}
render(h(App), nodeOps.createElement('div'))
expect(handler).toHaveBeenCalled()
})
})

View File

@@ -5,28 +5,33 @@ import { mockWarn } from '@vue/shared'
import { render, defineComponent, h, nodeOps } from '@vue/runtime-test'
import { isEmitListener } from '../src/componentEmits'
describe('emits option', () => {
describe('component: emit', () => {
mockWarn()
test('trigger both raw event and capitalize handlers', () => {
test('trigger handlers', () => {
const Foo = defineComponent({
render() {},
created() {
// the `emit` function is bound on component instances
this.$emit('foo')
this.$emit('bar')
this.$emit('!baz')
}
})
const onfoo = jest.fn()
const onBar = jest.fn()
const Comp = () => h(Foo, { onfoo, onBar })
const onBaz = jest.fn()
const Comp = () => h(Foo, { onfoo, onBar, ['on!baz']: onBaz })
render(h(Comp), nodeOps.createElement('div'))
expect(onfoo).toHaveBeenCalled()
expect(onfoo).not.toHaveBeenCalled()
// only capitalized or special chars are considerd event listeners
expect(onBar).toHaveBeenCalled()
expect(onBaz).toHaveBeenCalled()
})
// for v-model:foo-bar usage in DOM templates
test('trigger hyphendated events for update:xxx events', () => {
const Foo = defineComponent({
render() {},
@@ -49,6 +54,33 @@ describe('emits option', () => {
expect(barSpy).toHaveBeenCalled()
})
test('should trigger array of listeners', async () => {
const Child = defineComponent({
setup(_, { emit }) {
emit('foo', 1)
return () => h('div')
}
})
const fn1 = jest.fn()
const fn2 = jest.fn()
const App = {
setup() {
return () =>
h(Child, {
onFoo: [fn1, fn2]
})
}
}
render(h(App), nodeOps.createElement('div'))
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith(1)
expect(fn2).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith(1)
})
test('warning for undeclared event (array)', () => {
const Foo = defineComponent({
emits: ['foo'],
@@ -97,9 +129,9 @@ describe('emits option', () => {
test('isEmitListener', () => {
expect(isEmitListener(['click'], 'onClick')).toBe(true)
expect(isEmitListener(['click'], 'onclick')).toBe(true)
expect(isEmitListener(['click'], 'onclick')).toBe(false)
expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
expect(isEmitListener({ click: null }, 'onclick')).toBe(true)
expect(isEmitListener({ click: null }, 'onclick')).toBe(false)
expect(isEmitListener(['click'], 'onBlick')).toBe(false)
expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
})

View File

@@ -20,7 +20,7 @@ describe('component props', () => {
let proxy: any
const Comp = defineComponent({
props: ['foo'],
props: ['fooBar'],
render() {
props = this.$props
attrs = this.$attrs
@@ -29,18 +29,25 @@ describe('component props', () => {
})
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 1, bar: 2 }), root)
expect(proxy.foo).toBe(1)
expect(props).toEqual({ foo: 1 })
render(h(Comp, { fooBar: 1, bar: 2 }), root)
expect(proxy.fooBar).toBe(1)
expect(props).toEqual({ fooBar: 1 })
expect(attrs).toEqual({ bar: 2 })
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
expect(proxy.foo).toBe(2)
expect(props).toEqual({ foo: 2 })
// test passing kebab-case and resolving to camelCase
render(h(Comp, { 'foo-bar': 2, bar: 3, baz: 4 }), root)
expect(proxy.fooBar).toBe(2)
expect(props).toEqual({ fooBar: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
// test updating kebab-case should not delete it (#955)
render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4 }), root)
expect(proxy.fooBar).toBe(3)
expect(props).toEqual({ fooBar: 3 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render(h(Comp, { qux: 5 }), root)
expect(proxy.foo).toBeUndefined()
expect(proxy.fooBar).toBeUndefined()
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})

View File

@@ -0,0 +1,47 @@
import { ref, render, h, nodeOps, nextTick } from '@vue/runtime-test'
describe('component: slots', () => {
// TODO more tests for slots normalization etc.
test('should respect $stable flag', async () => {
const flag1 = ref(1)
const flag2 = ref(2)
const spy = jest.fn()
const Child = () => {
spy()
return 'child'
}
const App = {
setup() {
return () => [
flag1.value,
h(
Child,
{ n: flag2.value },
{
foo: () => 'foo',
$stable: true
}
)
]
}
}
render(h(App), nodeOps.createElement('div'))
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props didn't change, slots are stable
// -> child should not update
flag1.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props changed
// -> child should update
flag2.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
})
})

View File

@@ -416,21 +416,16 @@ describe('error handling', () => {
}
}
let res: any
const Child = {
props: ['onFoo'],
setup(props: any, { emit }: any) {
res = emit('foo')
emit('foo')
return () => null
}
}
render(h(Comp), nodeOps.createElement('div'))
try {
await Promise.all(res)
} catch (e) {
expect(e).toBe(err)
}
await nextTick()
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
})
@@ -438,6 +433,12 @@ describe('error handling', () => {
const err = new Error('foo')
const fn = jest.fn()
const res: Promise<any>[] = []
const createAsyncHandler = (p: Promise<any>) => () => {
res.push(p)
return p
}
const Comp = {
setup() {
onErrorCaptured((err, instance, info) => {
@@ -446,15 +447,17 @@ describe('error handling', () => {
})
return () =>
h(Child, {
onFoo: [() => Promise.reject(err), () => Promise.resolve(1)]
onFoo: [
createAsyncHandler(Promise.reject(err)),
createAsyncHandler(Promise.resolve(1))
]
})
}
}
let res: any
const Child = {
setup(props: any, { emit }: any) {
res = emit('foo')
emit('foo')
return () => null
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",
@@ -31,7 +31,7 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-core#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/reactivity": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/reactivity": "3.0.0-alpha.12"
}
}

View File

@@ -517,7 +517,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
return new Proxy(instance.slots, slotsHandlers)
},
get emit() {
return instance.emit
return (event: string, ...args: any[]) => instance.emit(event, ...args)
}
})
} else {

View File

@@ -28,12 +28,12 @@ export type EmitFn<
Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options
> = Options extends any[]
? (event: Options[0], ...args: any[]) => unknown[]
? (event: Options[0], ...args: any[]) => void
: UnionToIntersection<
{
[key in Event]: Options[key] extends ((...args: infer Args) => any)
? (event: key, ...args: Args) => unknown[]
: (event: key, ...args: any[]) => unknown[]
? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void
}[Event]
>
@@ -41,7 +41,7 @@ export function emit(
instance: ComponentInternalInstance,
event: string,
...args: any[]
): any[] {
) {
const props = instance.vnode.props || EMPTY_OBJ
if (__DEV__) {
@@ -66,23 +66,20 @@ export function emit(
}
}
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
let handler = props[`on${capitalize(event)}`]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && event.indexOf('update:') === 0) {
event = hyphenate(event)
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
handler = props[`on${capitalize(event)}`]
}
if (handler) {
const res = callWithAsyncErrorHandling(
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
return isArray(res) ? res : [res]
} else {
return []
}
}

View File

@@ -177,8 +177,15 @@ export function updateProps(
setFullProps(instance, rawProps, props, attrs)
// in case of dynamic props, check if we need to delete keys from
// the props object
let kebabKey: string
for (const key in rawCurrentProps) {
if (!rawProps || !hasOwn(rawProps, key)) {
if (
!rawProps ||
(!hasOwn(rawProps, key) &&
// it's possible the original props was passed in as kebab-case
// and converted to camelCase (#955)
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
) {
delete props[key]
}
}

View File

@@ -236,20 +236,12 @@ export function shouldUpdateComponent(
if (patchFlag & PatchFlags.FULL_PROPS) {
// presence of this flag indicates props are always non-null
return hasPropsChanged(prevProps!, nextProps!)
} else {
if (patchFlag & PatchFlags.CLASS) {
return prevProps!.class !== nextProps!.class
}
if (patchFlag & PatchFlags.STYLE) {
return hasPropsChanged(prevProps!.style, nextProps!.style)
}
if (patchFlag & PatchFlags.PROPS) {
const dynamicProps = nextVNode.dynamicProps!
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
if (nextProps![key] !== prevProps![key]) {
return true
}
} else if (patchFlag & PatchFlags.PROPS) {
const dynamicProps = nextVNode.dynamicProps!
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
if (nextProps![key] !== prevProps![key]) {
return true
}
}
}

View File

@@ -968,38 +968,7 @@ function baseCreateRenderer(
)
}
} else {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect runner.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.component = n1.component
n2.el = n1.el
}
updateComponent(n1, n2, parentComponent, optimized)
}
}
@@ -1077,6 +1046,45 @@ function baseCreateRenderer(
}
}
const updateComponent = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
optimized: boolean
) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect runner.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.component = n1.component
n2.el = n1.el
}
}
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,