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

# Conflicts:
#	packages/reactivity/src/ref.ts
#	packages/runtime-core/__tests__/apiTemplateRef.spec.ts
#	packages/runtime-core/src/apiWatch.ts
This commit is contained in:
pikax
2020-04-08 21:21:04 +01:00
339 changed files with 26645 additions and 8965 deletions

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer: portal should update children 1`] = `"<div>teleported</div>"`;
exports[`renderer: portal should update children 2`] = `""`;
exports[`renderer: portal should update children 3`] = `"teleported"`;
exports[`renderer: portal should update target 1`] = `"<!----><!--[object Object]--><div>root</div><!---->"`;
exports[`renderer: portal should update target 2`] = `"<div>teleported</div>"`;
exports[`renderer: portal should update target 3`] = `""`;
exports[`renderer: portal should update target 4`] = `"<!----><!--[object Object]--><div>root</div><!---->"`;
exports[`renderer: portal should update target 5`] = `""`;
exports[`renderer: portal should update target 6`] = `"<div>teleported</div>"`;
exports[`renderer: portal should work 1`] = `"<!----><!--[object Object]--><div>root</div><!---->"`;
exports[`renderer: portal should work 2`] = `"<div>teleported</div>"`;

View File

@@ -0,0 +1,610 @@
import {
defineAsyncComponent,
h,
Component,
ref,
nextTick,
Suspense
} from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
describe('api: defineAsyncComponent', () => {
test('simple usage', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent(
() =>
new Promise(r => {
resolve = r as any
})
)
const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
render: () => (toggle.value ? h(Foo) : null)
}).mount(root)
expect(serializeInner(root)).toBe('<!---->')
resolve!(() => 'resolved')
// first time resolve, wait for macro task since there are multiple
// microtasks / .then() calls
await timeout()
expect(serializeInner(root)).toBe('resolved')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// already resolved component should update on nextTick
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('resolved')
})
test('with loading component', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(r => {
resolve = r as any
}),
loadingComponent: () => 'loading',
delay: 1 // defaults to 200
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
render: () => (toggle.value ? h(Foo) : null)
}).mount(root)
// due to the delay, initial mount should be empty
expect(serializeInner(root)).toBe('<!---->')
// loading show up after delay
await timeout(1)
expect(serializeInner(root)).toBe('loading')
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// already resolved component should update on nextTick without loading
// state
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('resolved')
})
test('with loading component + explicit delay (0)', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(r => {
resolve = r as any
}),
loadingComponent: () => 'loading',
delay: 0
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
render: () => (toggle.value ? h(Foo) : null)
}).mount(root)
// with delay: 0, should show loading immediately
expect(serializeInner(root)).toBe('loading')
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// already resolved component should update on nextTick without loading
// state
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('resolved')
})
test('error without error component', async () => {
let resolve: (comp: Component) => void
let reject: (e: Error) => void
const Foo = defineAsyncComponent(
() =>
new Promise((_resolve, _reject) => {
resolve = _resolve as any
reject = _reject
})
)
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
render: () => (toggle.value ? h(Foo) : null)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
const err = new Error('foo')
reject!(err)
await timeout()
expect(handler).toHaveBeenCalled()
expect(handler.mock.calls[0][0]).toBe(err)
expect(serializeInner(root)).toBe('<!---->')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// errored out on previous load, toggle and mock success this time
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// should render this time
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('error with error component', async () => {
let resolve: (comp: Component) => void
let reject: (e: Error) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise((_resolve, _reject) => {
resolve = _resolve as any
reject = _reject
}),
errorComponent: (props: { error: Error }) => props.error.message
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
render: () => (toggle.value ? h(Foo) : null)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
const err = new Error('errored out')
reject!(err)
await timeout()
expect(handler).toHaveBeenCalled()
expect(serializeInner(root)).toBe('errored out')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// errored out on previous load, toggle and mock success this time
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// should render this time
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('error with error + loading components', async () => {
let resolve: (comp: Component) => void
let reject: (e: Error) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise((_resolve, _reject) => {
resolve = _resolve as any
reject = _reject
}),
errorComponent: (props: { error: Error }) => props.error.message,
loadingComponent: () => 'loading',
delay: 1
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
render: () => (toggle.value ? h(Foo) : null)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
// due to the delay, initial mount should be empty
expect(serializeInner(root)).toBe('<!---->')
// loading show up after delay
await timeout(1)
expect(serializeInner(root)).toBe('loading')
const err = new Error('errored out')
reject!(err)
await timeout()
expect(handler).toHaveBeenCalled()
expect(serializeInner(root)).toBe('errored out')
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// errored out on previous load, toggle and mock success this time
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
// loading show up after delay
await timeout(1)
expect(serializeInner(root)).toBe('loading')
// should render this time
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('timeout without error component', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(_resolve => {
resolve = _resolve as any
}),
timeout: 1
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
await timeout(1)
expect(handler).toHaveBeenCalled()
expect(handler.mock.calls[0][0].message).toMatch(
`Async component timed out after 1ms.`
)
expect(serializeInner(root)).toBe('<!---->')
// if it resolved after timeout, should still work
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('timeout with error component', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(_resolve => {
resolve = _resolve as any
}),
timeout: 1,
errorComponent: () => 'timed out'
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
await timeout(1)
expect(handler).toHaveBeenCalled()
expect(serializeInner(root)).toBe('timed out')
// if it resolved after timeout, should still work
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('timeout with error + loading components', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(_resolve => {
resolve = _resolve as any
}),
delay: 1,
timeout: 16,
errorComponent: () => 'timed out',
loadingComponent: () => 'loading'
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
await timeout(1)
expect(serializeInner(root)).toBe('loading')
await timeout(16)
expect(serializeInner(root)).toBe('timed out')
expect(handler).toHaveBeenCalled()
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('timeout without error component, but with loading component', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(_resolve => {
resolve = _resolve as any
}),
delay: 1,
timeout: 16,
loadingComponent: () => 'loading'
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
await timeout(1)
expect(serializeInner(root)).toBe('loading')
await timeout(16)
expect(handler).toHaveBeenCalled()
expect(handler.mock.calls[0][0].message).toMatch(
`Async component timed out after 16ms.`
)
// should still display loading
expect(serializeInner(root)).toBe('loading')
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved')
})
test('with suspense', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent(
() =>
new Promise(_resolve => {
resolve = _resolve as any
})
)
const root = nodeOps.createElement('div')
const app = createApp({
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
fallback: () => 'loading'
})
})
app.mount(root)
expect(serializeInner(root)).toBe('loading')
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved & resolved')
})
test('suspensible: false', async () => {
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent({
loader: () =>
new Promise(_resolve => {
resolve = _resolve as any
}),
suspensible: false
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
fallback: () => 'loading'
})
})
app.mount(root)
// should not show suspense fallback
expect(serializeInner(root)).toBe('<!----> & <!---->')
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved & resolved')
})
test('suspense with error handling', async () => {
let reject: (e: Error) => void
const Foo = defineAsyncComponent(
() =>
new Promise((_resolve, _reject) => {
reject = _reject
})
)
const root = nodeOps.createElement('div')
const app = createApp({
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
fallback: () => 'loading'
})
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('loading')
reject!(new Error('no'))
await timeout()
expect(handler).toHaveBeenCalled()
expect(serializeInner(root)).toBe('<!----> & <!---->')
})
test('retry (success)', async () => {
let loaderCallCount = 0
let resolve: (comp: Component) => void
let reject: (e: Error) => void
const Foo = defineAsyncComponent({
loader: () => {
loaderCallCount++
return new Promise((_resolve, _reject) => {
resolve = _resolve as any
reject = _reject
})
},
onError(error, retry, fail) {
if (error.message.match(/foo/)) {
retry()
} else {
fail()
}
}
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
expect(loaderCallCount).toBe(1)
const err = new Error('foo')
reject!(err)
await timeout()
expect(handler).not.toHaveBeenCalled()
expect(loaderCallCount).toBe(2)
expect(serializeInner(root)).toBe('<!---->')
// should render this time
resolve!(() => 'resolved')
await timeout()
expect(handler).not.toHaveBeenCalled()
expect(serializeInner(root)).toBe('resolved')
})
test('retry (skipped)', async () => {
let loaderCallCount = 0
let reject: (e: Error) => void
const Foo = defineAsyncComponent({
loader: () => {
loaderCallCount++
return new Promise((_resolve, _reject) => {
reject = _reject
})
},
onError(error, retry, fail) {
if (error.message.match(/bar/)) {
retry()
} else {
fail()
}
}
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
expect(loaderCallCount).toBe(1)
const err = new Error('foo')
reject!(err)
await timeout()
// should fail because retryWhen returns false
expect(handler).toHaveBeenCalled()
expect(handler.mock.calls[0][0]).toBe(err)
expect(loaderCallCount).toBe(1)
expect(serializeInner(root)).toBe('<!---->')
})
test('retry (fail w/ max retry attempts)', async () => {
let loaderCallCount = 0
let reject: (e: Error) => void
const Foo = defineAsyncComponent({
loader: () => {
loaderCallCount++
return new Promise((_resolve, _reject) => {
reject = _reject
})
},
onError(error, retry, fail, attempts) {
if (error.message.match(/foo/) && attempts <= 1) {
retry()
} else {
fail()
}
}
})
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
app.mount(root)
expect(serializeInner(root)).toBe('<!---->')
expect(loaderCallCount).toBe(1)
// first retry
const err = new Error('foo')
reject!(err)
await timeout()
expect(handler).not.toHaveBeenCalled()
expect(loaderCallCount).toBe(2)
expect(serializeInner(root)).toBe('<!---->')
// 2nd retry, should fail due to reaching maxRetries
reject!(err)
await timeout()
expect(handler).toHaveBeenCalled()
expect(handler.mock.calls[0][0]).toBe(err)
expect(loaderCallCount).toBe(2)
expect(serializeInner(root)).toBe('<!---->')
})
})

View File

@@ -3,7 +3,6 @@ import {
h,
nodeOps,
serializeInner,
mockWarn,
provide,
inject,
resolveComponent,
@@ -11,46 +10,64 @@ import {
withDirectives,
Plugin,
ref,
getCurrentInstance
getCurrentInstance,
defineComponent
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
describe('api: createApp', () => {
mockWarn()
test('mount', () => {
const Comp = {
const Comp = defineComponent({
props: {
count: {
default: 0
}
},
setup(props: { count: number }) {
setup(props) {
return () => props.count
}
}
})
const root1 = nodeOps.createElement('div')
createApp().mount(Comp, root1)
createApp(Comp).mount(root1)
expect(serializeInner(root1)).toBe(`0`)
// mount with props
const root2 = nodeOps.createElement('div')
const app2 = createApp()
app2.mount(Comp, root2, { count: 1 })
const app2 = createApp(Comp, { count: 1 })
app2.mount(root2)
expect(serializeInner(root2)).toBe(`1`)
// remount warning
const root3 = nodeOps.createElement('div')
app2.mount(Comp, root3)
app2.mount(root3)
expect(serializeInner(root3)).toBe(``)
expect(`already been mounted`).toHaveBeenWarned()
})
test('provide', () => {
const app = createApp()
app.provide('foo', 1)
app.provide('bar', 2)
test('unmount', () => {
const Comp = defineComponent({
props: {
count: {
default: 0
}
},
setup(props) {
return () => props.count
}
})
const root = nodeOps.createElement('div')
const app = createApp(Comp)
app.mount(root)
app.unmount(root)
expect(serializeInner(root)).toBe(``)
})
test('provide', () => {
const Root = {
setup() {
// test override
@@ -63,29 +80,24 @@ describe('api: createApp', () => {
setup() {
const foo = inject('foo')
const bar = inject('bar')
try {
inject('__proto__')
} catch (e) {}
return () => `${foo},${bar}`
}
}
const app = createApp(Root)
app.provide('foo', 1)
app.provide('bar', 2)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(serializeInner(root)).toBe(`3,2`)
expect('[Vue warn]: injection "__proto__" not found.').toHaveBeenWarned()
})
test('component', () => {
const app = createApp()
const FooBar = () => 'foobar!'
app.component('FooBar', FooBar)
expect(app.component('FooBar')).toBe(FooBar)
app.component('BarBaz', () => 'barbaz!')
app.component('BarBaz', () => 'barbaz!')
expect(
'Component "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const Root = {
// local override
components: {
@@ -102,33 +114,29 @@ describe('api: createApp', () => {
}
}
const app = createApp(Root)
const FooBar = () => 'foobar!'
app.component('FooBar', FooBar)
expect(app.component('FooBar')).toBe(FooBar)
app.component('BarBaz', () => 'barbaz!')
app.component('BarBaz', () => 'barbaz!')
expect(
'Component "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
})
test('directive', () => {
const app = createApp()
const spy1 = jest.fn()
const spy2 = jest.fn()
const spy3 = jest.fn()
const FooBar = { mounted: spy1 }
app.directive('FooBar', FooBar)
expect(app.directive('FooBar')).toBe(FooBar)
app.directive('BarBaz', {
mounted: spy2
})
app.directive('BarBaz', {
mounted: spy2
})
expect(
'Directive "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const Root = {
// local override
directives: {
@@ -145,8 +153,25 @@ describe('api: createApp', () => {
}
}
const app = createApp(Root)
const FooBar = { mounted: spy1 }
app.directive('FooBar', FooBar)
expect(app.directive('FooBar')).toBe(FooBar)
app.directive('BarBaz', {
mounted: spy2
})
app.directive('BarBaz', {
mounted: spy2
})
expect(
'Directive "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(spy1).toHaveBeenCalled()
expect(spy2).not.toHaveBeenCalled()
expect(spy3).toHaveBeenCalled()
@@ -212,7 +237,7 @@ describe('api: createApp', () => {
}
}
const app = createApp()
const app = createApp(Comp)
app.mixin(mixinA)
app.mixin(mixinB)
@@ -226,7 +251,7 @@ describe('api: createApp', () => {
).toHaveBeenWarnedTimes(1)
const root = nodeOps.createElement('div')
app.mount(Comp, root)
app.mount(root)
expect(serializeInner(root)).toBe(`123`)
expect(calls).toEqual([
@@ -242,13 +267,15 @@ describe('api: createApp', () => {
test('use', () => {
const PluginA: Plugin = app => app.provide('foo', 1)
const PluginB: Plugin = {
install: app => app.provide('bar', 2)
install: (app, arg1, arg2) => app.provide('bar', arg1 + arg2)
}
const PluginC: any = undefined
const app = createApp()
app.use(PluginA)
app.use(PluginB)
class PluginC {
someProperty = {}
static install() {
app.provide('baz', 2)
}
}
const PluginD: any = undefined
const Root = {
setup() {
@@ -257,8 +284,14 @@ describe('api: createApp', () => {
return () => `${foo},${bar}`
}
}
const app = createApp(Root)
app.use(PluginA)
app.use(PluginB, 1, 1)
app.use(PluginC)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(serializeInner(root)).toBe(`1,2`)
app.use(PluginA)
@@ -266,7 +299,7 @@ describe('api: createApp', () => {
`Plugin has already been applied to target app`
).toHaveBeenWarnedTimes(1)
app.use(PluginC)
app.use(PluginD)
expect(
`A plugin must either be a function or an object with an "install" ` +
`function.`
@@ -274,18 +307,14 @@ describe('api: createApp', () => {
})
test('config.errorHandler', () => {
const app = createApp()
const error = new Error()
const count = ref(0)
const handler = (app.config.errorHandler = jest.fn(
(err, instance, info) => {
expect(err).toBe(error)
expect((instance as any).count).toBe(count.value)
expect(info).toBe(`render function`)
}
))
const handler = jest.fn((err, instance, info) => {
expect(err).toBe(error)
expect((instance as any).count).toBe(count.value)
expect(info).toBe(`render function`)
})
const Root = {
setup() {
@@ -299,21 +328,19 @@ describe('api: createApp', () => {
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
app.config.errorHandler = handler
app.mount(nodeOps.createElement('div'))
expect(handler).toHaveBeenCalled()
})
test('config.warnHandler', () => {
const app = createApp()
let ctx: any
const handler = (app.config.warnHandler = jest.fn(
(msg, instance, trace) => {
expect(msg).toMatch(`Component is missing template or render function`)
expect(instance).toBe(ctx.proxy)
expect(trace).toMatch(`Hello`)
}
))
const handler = jest.fn((msg, instance, trace) => {
expect(msg).toMatch(`Component is missing template or render function`)
expect(instance).toBe(ctx.proxy)
expect(trace).toMatch(`Hello`)
})
const Root = {
name: 'Hello',
@@ -322,7 +349,9 @@ describe('api: createApp', () => {
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
app.config.warnHandler = handler
app.mount(nodeOps.createElement('div'))
expect(handler).toHaveBeenCalledTimes(1)
})
@@ -330,107 +359,119 @@ describe('api: createApp', () => {
const isNativeTag = jest.fn(tag => tag === 'div')
test('Component.name', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
name: 'div',
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
test('Component.components', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
components: {
div: () => 'div'
},
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
test('Component.directives', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
directives: {
bind: () => {}
},
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in directive ids as custom directive id: bind`
).toHaveBeenWarned()
})
test('register using app.component', () => {
const app = createApp()
const app = createApp({
render() {}
})
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.component('div', () => 'div')
app.mount(Root, nodeOps.createElement('div'))
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
})
test('config.optionMergeStrategies', () => {
let merged: string
const App = defineComponent({
render() {},
mixins: [{ foo: 'mixin' }],
extends: { foo: 'extends' },
foo: 'local',
beforeCreate() {
merged = this.$options.foo
}
})
const app = createApp(App)
app.mixin({
foo: 'global'
})
app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
app.mount(nodeOps.createElement('div'))
expect(merged!).toBe('global,extends,mixin,local')
})
test('config.globalProperties', () => {
const app = createApp({
render() {
return this.foo
}
})
app.config.globalProperties.foo = 'hello'
const root = nodeOps.createElement('div')
app.mount(root)
expect(serializeInner(root)).toBe('hello')
})
})

View File

@@ -9,7 +9,8 @@ import {
readonly,
reactive
} from '../src/index'
import { render, nodeOps, serialize, mockWarn } from '@vue/runtime-test'
import { render, nodeOps, serialize } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
@@ -283,4 +284,27 @@ describe('api: provide/inject', () => {
expect(serialize(root)).toBe(`<div><!----></div>`)
expect(`injection "foo" not found.`).toHaveBeenWarned()
})
it('should not warn when default value is undefined', () => {
const Provider = {
setup() {
return () => h(Middle)
}
}
const Middle = {
render: () => h(Consumer)
}
const Consumer = {
setup() {
const foo = inject('foo', undefined)
return () => foo
}
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
})
})

View File

@@ -8,13 +8,13 @@ import {
nextTick,
renderToString,
ref,
createComponent,
mockWarn
defineComponent
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
describe('api: options', () => {
test('data', async () => {
const Comp = createComponent({
const Comp = defineComponent({
data() {
return {
foo: 1
@@ -42,7 +42,7 @@ describe('api: options', () => {
})
test('computed', async () => {
const Comp = createComponent({
const Comp = defineComponent({
data() {
return {
foo: 1
@@ -52,9 +52,7 @@ describe('api: options', () => {
bar(): number {
return this.foo + 1
},
baz(): number {
return this.bar + 1
}
baz: (vm): number => vm.bar + 1
},
render() {
return h(
@@ -78,7 +76,7 @@ describe('api: options', () => {
})
test('methods', async () => {
const Comp = createComponent({
const Comp = defineComponent({
data() {
return {
foo: 1
@@ -149,30 +147,24 @@ describe('api: options', () => {
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
expect(spy).toHaveReturnedWith(ctx)
}
assertCall(spyA, 0, [1, undefined])
assertCall(spyB, 0, [2, undefined])
assertCall(spyC, 0, [{ qux: 3 }, undefined])
expect(spyA).toHaveReturnedWith(ctx)
expect(spyB).toHaveReturnedWith(ctx)
expect(spyC).toHaveReturnedWith(ctx)
ctx.foo++
await nextTick()
expect(spyA).toHaveBeenCalledTimes(2)
assertCall(spyA, 1, [2, 1])
expect(spyA).toHaveBeenCalledTimes(1)
assertCall(spyA, 0, [2, 1])
ctx.bar++
await nextTick()
expect(spyB).toHaveBeenCalledTimes(2)
assertCall(spyB, 1, [3, 2])
expect(spyB).toHaveBeenCalledTimes(1)
assertCall(spyB, 0, [3, 2])
ctx.baz.qux++
await nextTick()
expect(spyC).toHaveBeenCalledTimes(2)
expect(spyC).toHaveBeenCalledTimes(1)
// new and old objects have same identity
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
})
test('watch array', async () => {
@@ -218,30 +210,24 @@ describe('api: options', () => {
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
expect(spy).toHaveReturnedWith(ctx)
}
assertCall(spyA, 0, [1, undefined])
assertCall(spyB, 0, [2, undefined])
assertCall(spyC, 0, [{ qux: 3 }, undefined])
expect(spyA).toHaveReturnedWith(ctx)
expect(spyB).toHaveReturnedWith(ctx)
expect(spyC).toHaveReturnedWith(ctx)
ctx.foo++
await nextTick()
expect(spyA).toHaveBeenCalledTimes(2)
assertCall(spyA, 1, [2, 1])
expect(spyA).toHaveBeenCalledTimes(1)
assertCall(spyA, 0, [2, 1])
ctx.bar++
await nextTick()
expect(spyB).toHaveBeenCalledTimes(2)
assertCall(spyB, 1, [3, 2])
expect(spyB).toHaveBeenCalledTimes(1)
assertCall(spyB, 0, [3, 2])
ctx.baz.qux++
await nextTick()
expect(spyC).toHaveBeenCalledTimes(2)
expect(spyC).toHaveBeenCalledTimes(1)
// new and old objects have same identity
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
})
test('provide/inject', () => {
@@ -295,7 +281,7 @@ describe('api: options', () => {
}
} as any
expect(renderToString(h(Root))).toBe(`<!---->1112<!---->`)
expect(renderToString(h(Root))).toBe(`1112`)
})
test('lifecycle', async () => {
@@ -536,7 +522,7 @@ describe('api: options', () => {
})
test('accessing setup() state from options', async () => {
const Comp = createComponent({
const Comp = defineComponent({
setup() {
return {
count: ref(0)
@@ -648,9 +634,9 @@ describe('api: options', () => {
test('data property is already declared in props', () => {
const Comp = {
props: { foo: Number },
data: {
data: () => ({
foo: 1
},
}),
render() {}
}
@@ -663,9 +649,9 @@ describe('api: options', () => {
test('computed property is already declared in data', () => {
const Comp = {
data: {
data: () => ({
foo: 1
},
}),
computed: {
foo() {}
},
@@ -713,9 +699,9 @@ describe('api: options', () => {
test('methods property is already declared in data', () => {
const Comp = {
data: {
data: () => ({
foo: 2
},
}),
methods: {
foo() {}
},

View File

@@ -6,8 +6,8 @@ import {
render,
serializeInner,
nextTick,
watch,
createComponent,
watchEffect,
defineComponent,
triggerEvent,
TestElement
} from '@vue/runtime-test'
@@ -16,7 +16,7 @@ import {
describe('api: setup context', () => {
it('should expose return values to template render context', () => {
const Comp = createComponent({
const Comp = defineComponent({
setup() {
return {
// ref should auto-unwrap
@@ -53,9 +53,10 @@ describe('api: setup context', () => {
render: () => h(Child, { count: count.value })
}
const Child = createComponent({
setup(props: { count: number }) {
watch(() => {
const Child = defineComponent({
props: { count: Number },
setup(props) {
watchEffect(() => {
dummy = props.count
})
return () => h('div', props.count)
@@ -82,13 +83,13 @@ describe('api: setup context', () => {
render: () => h(Child, { count: count.value })
}
const Child = createComponent({
const Child = defineComponent({
props: {
count: Number
},
setup(props) {
watch(() => {
watchEffect(() => {
dummy = props.count
})
return () => h('div', props.count)
@@ -119,7 +120,6 @@ describe('api: setup context', () => {
// puts everything received in attrs
// disable implicit fallthrough
inheritAttrs: false,
props: {},
setup(props: any, { attrs }: any) {
return () => h('div', attrs)
}
@@ -177,7 +177,7 @@ describe('api: setup context', () => {
})
}
const Child = createComponent({
const Child = defineComponent({
props: {
count: {
type: Number,

View File

@@ -4,7 +4,9 @@ import {
h,
render,
nextTick,
createComponent
Ref,
defineComponent,
reactive
} from '@vue/runtime-test'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
@@ -82,7 +84,7 @@ describe('api: template refs', () => {
const root = nodeOps.createElement('div')
const fn = jest.fn()
const Comp = createComponent(() => () => h('div', { ref: fn }))
const Comp = defineComponent(() => () => h('div', { ref: fn }))
render(h(Comp), root)
expect(fn.mock.calls[0][0]).toBe(root.children[0])
})
@@ -93,7 +95,7 @@ describe('api: template refs', () => {
const fn2 = jest.fn()
const fn = ref(fn1)
const Comp = createComponent(() => () => h('div', { ref: fn.value }))
const Comp = defineComponent(() => () => h('div', { ref: fn.value }))
render(h(Comp), root)
expect(fn1.mock.calls).toHaveLength(1)
@@ -112,7 +114,7 @@ describe('api: template refs', () => {
const fn = jest.fn()
const toggle = ref(true)
const Comp = createComponent(() => () =>
const Comp = defineComponent(() => () =>
toggle.value ? h('div', { ref: fn }) : null
)
render(h(Comp), root)
@@ -175,4 +177,46 @@ describe('api: template refs', () => {
await nextTick()
expect(el.value).toBe(null)
})
test('string ref inside slots', async () => {
const root = nodeOps.createElement('div')
const spy = jest.fn()
const Child = {
render(this: any) {
return this.$slots.default()
}
}
const Comp = {
render() {
return h(Child, () => {
return h('div', { ref: 'foo' })
})
},
mounted(this: any) {
spy(this.$refs.foo.tag)
}
}
render(h(Comp), root)
expect(spy).toHaveBeenCalledWith('div')
})
it('should work with direct reactive property', () => {
const root = nodeOps.createElement('div')
const state = reactive({
refKey: null
})
const Comp = {
setup() {
return state
},
render() {
return h('div', { ref: 'refKey' })
}
}
render(h(Comp), root)
expect(state.refKey).toBe(root.children[0])
})
})

View File

@@ -1,4 +1,12 @@
import { watch, reactive, computed, nextTick, ref, h } from '../src/index'
import {
watch,
watchEffect,
reactive,
computed,
nextTick,
ref,
h
} from '../src/index'
import { render, nodeOps, serializeInner } from '@vue/runtime-test'
import {
ITERATE_KEY,
@@ -6,17 +14,19 @@ import {
TrackOpTypes,
TriggerOpTypes
} from '@vue/reactivity'
import { mockWarn } from '@vue/shared'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
describe('api: watch', () => {
it('basic usage', async () => {
mockWarn()
it('effect', async () => {
const state = reactive({ count: 0 })
let dummy
watch(() => {
watchEffect(() => {
dummy = state.count
})
await nextTick()
expect(dummy).toBe(0)
state.count++
@@ -33,12 +43,11 @@ describe('api: watch', () => {
dummy = [count, prevCount]
// assert types
count + 1
prevCount + 1
if (prevCount) {
prevCount + 1
}
}
)
await nextTick()
expect(dummy).toMatchObject([0, undefined])
state.count++
await nextTick()
expect(dummy).toMatchObject([1, 0])
@@ -51,11 +60,10 @@ describe('api: watch', () => {
dummy = [count, prevCount]
// assert types
count + 1
prevCount + 1
if (prevCount) {
prevCount + 1
}
})
await nextTick()
expect(dummy).toMatchObject([0, undefined])
count.value++
await nextTick()
expect(dummy).toMatchObject([1, 0])
@@ -69,11 +77,10 @@ describe('api: watch', () => {
dummy = [count, prevCount]
// assert types
count + 1
prevCount + 1
if (prevCount) {
prevCount + 1
}
})
await nextTick()
expect(dummy).toMatchObject([1, undefined])
count.value++
await nextTick()
expect(dummy).toMatchObject([2, 1])
@@ -91,8 +98,6 @@ describe('api: watch', () => {
vals.concat(1)
oldVals.concat(1)
})
await nextTick()
expect(dummy).toMatchObject([[1, 1, 2], []])
state.count++
count.value++
@@ -107,28 +112,25 @@ describe('api: watch', () => {
let dummy
watch([() => state.count, status] as const, (vals, oldVals) => {
dummy = [vals, oldVals]
let [count] = vals
let [, oldStatus] = oldVals
const [count] = vals
const [, oldStatus] = oldVals
// assert types
count + 1
oldStatus === true
})
await nextTick()
expect(dummy).toMatchObject([[1, false], []])
state.count++
status.value = false
status.value = true
await nextTick()
expect(dummy).toMatchObject([[2, false], [1, false]])
expect(dummy).toMatchObject([[2, true], [1, false]])
})
it('stopping the watcher', async () => {
it('stopping the watcher (effect)', async () => {
const state = reactive({ count: 0 })
let dummy
const stop = watch(() => {
const stop = watchEffect(() => {
dummy = state.count
})
await nextTick()
expect(dummy).toBe(0)
stop()
@@ -138,15 +140,35 @@ describe('api: watch', () => {
expect(dummy).toBe(0)
})
it('cleanup registration (basic)', async () => {
it('stopping the watcher (with source)', async () => {
const state = reactive({ count: 0 })
let dummy
const stop = watch(
() => state.count,
count => {
dummy = count
}
)
state.count++
await nextTick()
expect(dummy).toBe(1)
stop()
state.count++
await nextTick()
// should not update
expect(dummy).toBe(1)
})
it('cleanup registration (effect)', async () => {
const state = reactive({ count: 0 })
const cleanup = jest.fn()
let dummy
const stop = watch(onCleanup => {
const stop = watchEffect(onCleanup => {
onCleanup(cleanup)
dummy = state.count
})
await nextTick()
expect(dummy).toBe(0)
state.count++
@@ -166,27 +188,35 @@ describe('api: watch', () => {
onCleanup(cleanup)
dummy = count
})
count.value++
await nextTick()
expect(dummy).toBe(0)
expect(cleanup).toHaveBeenCalledTimes(0)
expect(dummy).toBe(1)
count.value++
await nextTick()
expect(cleanup).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
expect(dummy).toBe(2)
stop()
expect(cleanup).toHaveBeenCalledTimes(2)
})
it('flush timing: post', async () => {
it('flush timing: post (default)', async () => {
const count = ref(0)
let callCount = 0
const assertion = jest.fn(count => {
expect(serializeInner(root)).toBe(`${count}`)
callCount++
// on mount, the watcher callback should be called before DOM render
// on update, should be called after the count is updated
const expectedDOM = callCount === 1 ? `` : `${count}`
expect(serializeInner(root)).toBe(expectedDOM)
})
const Comp = {
setup() {
watch(() => {
watchEffect(() => {
assertion(count.value)
})
return () => count.value
@@ -194,7 +224,6 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(assertion).toHaveBeenCalledTimes(1)
count.value++
@@ -221,7 +250,7 @@ describe('api: watch', () => {
const Comp = {
setup() {
watch(
watchEffect(
() => {
assertion(count.value, count2.value)
},
@@ -234,7 +263,6 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(assertion).toHaveBeenCalledTimes(1)
count.value++
@@ -264,7 +292,7 @@ describe('api: watch', () => {
const Comp = {
setup() {
watch(
watchEffect(
() => {
assertion(count.value)
},
@@ -277,7 +305,6 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(assertion).toHaveBeenCalledTimes(1)
count.value++
@@ -310,9 +337,6 @@ describe('api: watch', () => {
{ deep: true }
)
await nextTick()
expect(dummy).toEqual([0, 1, 1, true])
state.nested.count++
await nextTick()
expect(dummy).toEqual([1, 1, 1, true])
@@ -333,15 +357,75 @@ describe('api: watch', () => {
expect(dummy).toEqual([1, 2, 2, false])
})
it('lazy', async () => {
it('immediate', async () => {
const count = ref(0)
const cb = jest.fn()
watch(count, cb, { lazy: true })
await nextTick()
expect(cb).not.toHaveBeenCalled()
watch(count, cb, { immediate: true })
expect(cb).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(cb).toHaveBeenCalled()
expect(cb).toHaveBeenCalledTimes(2)
})
it('immediate: triggers when initial value is null', async () => {
const state = ref(null)
const spy = jest.fn()
watch(() => state.value, spy, { immediate: true })
expect(spy).toHaveBeenCalled()
})
it('immediate: triggers when initial value is undefined', async () => {
const state = ref()
const spy = jest.fn()
watch(() => state.value, spy, { immediate: true })
expect(spy).toHaveBeenCalled()
state.value = 3
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
// testing if undefined can trigger the watcher
state.value = undefined
await nextTick()
expect(spy).toHaveBeenCalledTimes(3)
// it shouldn't trigger if the same value is set
state.value = undefined
await nextTick()
expect(spy).toHaveBeenCalledTimes(3)
})
it('warn immediate option when using effect', async () => {
const count = ref(0)
let dummy
watchEffect(
() => {
dummy = count.value
},
// @ts-ignore
{ immediate: false }
)
expect(dummy).toBe(0)
expect(`"immediate" option is only respected`).toHaveBeenWarned()
count.value++
await nextTick()
expect(dummy).toBe(1)
})
it('warn and not respect deep option when using effect', async () => {
const arr = ref([1, [2]])
const spy = jest.fn()
watchEffect(
() => {
spy()
return arr
},
// @ts-ignore
{ deep: true }
)
expect(spy).toHaveBeenCalledTimes(1)
;(arr.value[1] as Array<number>)[0] = 3
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
expect(`"deep" option is only respected`).toHaveBeenWarned()
})
it('onTrack', async () => {
@@ -351,7 +435,7 @@ describe('api: watch', () => {
events.push(e)
})
const obj = reactive({ foo: 1, bar: 2 })
watch(
watchEffect(
() => {
dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
},
@@ -386,7 +470,7 @@ describe('api: watch', () => {
events.push(e)
})
const obj = reactive({ foo: 1 })
watch(
watchEffect(
() => {
dummy = obj.foo
},

View File

@@ -1,4 +1,11 @@
import { h, ref, render, nodeOps, nextTick } from '@vue/runtime-test'
import {
h,
ref,
render,
nodeOps,
nextTick,
defineComponent
} from '@vue/runtime-test'
describe('renderer: component', () => {
test.todo('should work')
@@ -7,7 +14,34 @@ describe('renderer: component', () => {
test.todo('componentProxy')
test.todo('componentProps')
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 () => {
@@ -52,4 +86,61 @@ describe('renderer: component', () => {
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

@@ -0,0 +1,106 @@
// Note: emits and listener fallthrough is tested in
// ./rendererAttrsFallthrough.spec.ts.
import { mockWarn } from '@vue/shared'
import { render, defineComponent, h, nodeOps } from '@vue/runtime-test'
import { isEmitListener } from '../src/componentEmits'
describe('emits option', () => {
mockWarn()
test('trigger both raw event and capitalize handlers', () => {
const Foo = defineComponent({
render() {},
created() {
// the `emit` function is bound on component instances
this.$emit('foo')
this.$emit('bar')
}
})
const onfoo = jest.fn()
const onBar = jest.fn()
const Comp = () => h(Foo, { onfoo, onBar })
render(h(Comp), nodeOps.createElement('div'))
expect(onfoo).toHaveBeenCalled()
expect(onBar).toHaveBeenCalled()
})
test('trigger hyphendated events for update:xxx events', () => {
const Foo = defineComponent({
render() {},
created() {
this.$emit('update:fooProp')
this.$emit('update:barProp')
}
})
const fooSpy = jest.fn()
const barSpy = jest.fn()
const Comp = () =>
h(Foo, {
'onUpdate:fooProp': fooSpy,
'onUpdate:bar-prop': barSpy
})
render(h(Comp), nodeOps.createElement('div'))
expect(fooSpy).toHaveBeenCalled()
expect(barSpy).toHaveBeenCalled()
})
test('warning for undeclared event (array)', () => {
const Foo = defineComponent({
emits: ['foo'],
render() {},
created() {
// @ts-ignore
this.$emit('bar')
}
})
render(h(Foo), nodeOps.createElement('div'))
expect(
`Component emitted event "bar" but it is not declared`
).toHaveBeenWarned()
})
test('warning for undeclared event (object)', () => {
const Foo = defineComponent({
emits: {
foo: null
},
render() {},
created() {
// @ts-ignore
this.$emit('bar')
}
})
render(h(Foo), nodeOps.createElement('div'))
expect(
`Component emitted event "bar" but it is not declared`
).toHaveBeenWarned()
})
test('validator warning', () => {
const Foo = defineComponent({
emits: {
foo: (arg: number) => arg > 0
},
render() {},
created() {
this.$emit('foo', -1)
}
})
render(h(Foo), nodeOps.createElement('div'))
expect(`event validation failed for event "foo"`).toHaveBeenWarned()
})
test('isEmitListener', () => {
expect(isEmitListener(['click'], 'onClick')).toBe(true)
expect(isEmitListener(['click'], 'onclick')).toBe(true)
expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
expect(isEmitListener({ click: null }, 'onclick')).toBe(true)
expect(isEmitListener(['click'], 'onBlick')).toBe(false)
expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
})
})

View File

@@ -0,0 +1,232 @@
import {
ComponentInternalInstance,
getCurrentInstance,
render,
h,
nodeOps,
FunctionalComponent,
defineComponent,
ref
} from '@vue/runtime-test'
import { render as domRender, nextTick } from 'vue'
import { mockWarn } from '@vue/shared'
describe('component props', () => {
mockWarn()
test('stateful', () => {
let props: any
let attrs: any
let proxy: any
const Comp = defineComponent({
props: ['foo'],
render() {
props = this.$props
attrs = this.$attrs
proxy = this
}
})
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 1, bar: 2 }), root)
expect(proxy.foo).toBe(1)
expect(props).toEqual({ foo: 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 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render(h(Comp, { qux: 5 }), root)
expect(proxy.foo).toBeUndefined()
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})
test('stateful with setup', () => {
let props: any
let attrs: any
const Comp = defineComponent({
props: ['foo'],
setup(_props, { attrs: _attrs }) {
return () => {
props = _props
attrs = _attrs
}
}
})
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 1, bar: 2 }), root)
expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ bar: 2 })
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
expect(props).toEqual({ foo: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render(h(Comp, { qux: 5 }), root)
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})
test('functional with declaration', () => {
let props: any
let attrs: any
const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
props = _props
attrs = _attrs
}
Comp.props = ['foo']
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 1, bar: 2 }), root)
expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ bar: 2 })
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
expect(props).toEqual({ foo: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render(h(Comp, { qux: 5 }), root)
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})
test('functional without declaration', () => {
let props: any
let attrs: any
const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
props = _props
attrs = _attrs
}
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 1 }), root)
expect(props).toEqual({ foo: 1 })
expect(attrs).toEqual({ foo: 1 })
expect(props).toBe(attrs)
render(h(Comp, { bar: 2 }), root)
expect(props).toEqual({ bar: 2 })
expect(attrs).toEqual({ bar: 2 })
expect(props).toBe(attrs)
})
test('boolean casting', () => {
let proxy: any
const Comp = {
props: {
foo: Boolean,
bar: Boolean,
baz: Boolean,
qux: Boolean
},
render() {
proxy = this
}
}
render(
h(Comp, {
// absent should cast to false
bar: '', // empty string should cast to true
baz: 'baz', // same string should cast to true
qux: 'ok' // other values should be left in-tact (but raise warning)
}),
nodeOps.createElement('div')
)
expect(proxy.foo).toBe(false)
expect(proxy.bar).toBe(true)
expect(proxy.baz).toBe(true)
expect(proxy.qux).toBe('ok')
expect('type check failed for prop "qux"').toHaveBeenWarned()
})
test('default value', () => {
let proxy: any
const Comp = {
props: {
foo: {
default: 1
},
bar: {
default: () => ({ a: 1 })
}
},
render() {
proxy = this
}
}
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 2 }), root)
expect(proxy.foo).toBe(2)
expect(proxy.bar).toEqual({ a: 1 })
render(h(Comp, { foo: undefined, bar: { b: 2 } }), root)
expect(proxy.foo).toBe(1)
expect(proxy.bar).toEqual({ b: 2 })
})
test('optimized props updates', async () => {
const Child = defineComponent({
props: ['foo'],
template: `<div>{{ foo }}</div>`
})
const foo = ref(1)
const id = ref('a')
const Comp = defineComponent({
setup() {
return {
foo,
id
}
},
components: { Child },
template: `<Child :foo="foo" :id="id"/>`
})
// Note this one is using the main Vue render so it can compile template
// on the fly
const root = document.createElement('div')
domRender(h(Comp), root)
expect(root.innerHTML).toBe('<div id="a">1</div>')
foo.value++
await nextTick()
expect(root.innerHTML).toBe('<div id="a">2</div>')
id.value = 'b'
await nextTick()
expect(root.innerHTML).toBe('<div id="b">2</div>')
})
test('warn props mutation', () => {
let instance: ComponentInternalInstance
let setupProps: any
const Comp = {
props: ['foo'],
setup(props: any) {
instance = getCurrentInstance()!
setupProps = props
return () => null
}
}
render(h(Comp, { foo: 1 }), nodeOps.createElement('div'))
expect(setupProps.foo).toBe(1)
expect(instance!.props.foo).toBe(1)
setupProps.foo = 2
expect(`Set operation on key "foo" failed`).toHaveBeenWarned()
expect(() => {
;(instance!.proxy as any).foo = 2
}).toThrow(TypeError)
expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
})
})

View File

@@ -1,16 +1,17 @@
import {
createApp,
h,
render,
getCurrentInstance,
nodeOps,
mockWarn
createApp
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
import { ComponentInternalInstance } from '../src/component'
describe('component: proxy', () => {
mockWarn()
test('data', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -27,14 +28,13 @@ describe('component: proxy', () => {
return null
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
instanceProxy.foo = 2
expect(instance!.data.foo).toBe(2)
})
test('renderContext', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -51,40 +51,27 @@ describe('component: proxy', () => {
return null
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
instanceProxy.foo = 2
expect(instance!.renderContext.foo).toBe(2)
})
test('propsProxy', () => {
const app = createApp()
let instance: ComponentInternalInstance
test('should not expose non-declared props', () => {
let instanceProxy: any
const Comp = {
props: {
foo: {
type: Number,
default: 1
}
},
setup() {
return () => null
},
mounted() {
instance = getCurrentInstance()!
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
expect(instance!.propsProxy!.foo).toBe(1)
expect(() => (instanceProxy.foo = 2)).toThrow(TypeError)
expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
render(h(Comp, { count: 1 }), nodeOps.createElement('div'))
expect('count' in instanceProxy).toBe(false)
})
test('public properties', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -96,14 +83,16 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.$data).toBe(instance!.data)
expect(instanceProxy.$props).toBe(instance!.propsProxy)
expect(instanceProxy.$props).toBe(instance!.props)
expect(instanceProxy.$attrs).toBe(instance!.attrs)
expect(instanceProxy.$slots).toBe(instance!.slots)
expect(instanceProxy.$refs).toBe(instance!.refs)
expect(instanceProxy.$parent).toBe(instance!.parent)
expect(instanceProxy.$root).toBe(instance!.root)
expect(instanceProxy.$parent).toBe(
instance!.parent && instance!.parent.proxy
)
expect(instanceProxy.$root).toBe(instance!.root.proxy)
expect(instanceProxy.$emit).toBe(instance!.emit)
expect(instanceProxy.$el).toBe(instance!.vnode.el)
expect(instanceProxy.$options).toBe(instance!.type)
@@ -112,7 +101,6 @@ describe('component: proxy', () => {
})
test('sink', async () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -124,14 +112,40 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
instanceProxy.foo = 1
expect(instanceProxy.foo).toBe(1)
expect(instance!.sink.foo).toBe(1)
})
test('globalProperties', () => {
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
setup() {
return () => null
},
mounted() {
instance = getCurrentInstance()!
instanceProxy = this
}
}
const app = createApp(Comp)
app.config.globalProperties.foo = 1
app.mount(nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
// set should overwrite globalProperties with local
instanceProxy.foo = 2
expect(instanceProxy.foo).toBe(2)
expect(instance!.sink.foo).toBe(2)
// should not affect global
expect(app.config.globalProperties.foo).toBe(1)
})
test('has check', () => {
const app = createApp()
let instanceProxy: any
const Comp = {
render() {},
@@ -152,7 +166,11 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'), { msg: 'hello' })
const app = createApp(Comp, { msg: 'hello' })
app.config.globalProperties.global = 1
app.mount(nodeOps.createElement('div'))
// props
expect('msg' in instanceProxy).toBe(true)
@@ -162,6 +180,8 @@ describe('component: proxy', () => {
expect('bar' in instanceProxy).toBe(true)
// public properties
expect('$el' in instanceProxy).toBe(true)
// global properties
expect('global' in instanceProxy).toBe(true)
// non-existent
expect('$foobar' in instanceProxy).toBe(false)
@@ -170,5 +190,28 @@ describe('component: proxy', () => {
// set non-existent (goes into sink)
instanceProxy.baz = 1
expect('baz' in instanceProxy).toBe(true)
// dev mode ownKeys check for console inspection
// should only expose own keys
expect(Object.keys(instanceProxy)).toMatchObject([
'msg',
'bar',
'foo',
'baz'
])
})
// #864
test('should not warn declared but absent props', () => {
const Comp = {
props: ['test'],
render(this: any) {
return this.test
}
}
render(h(Comp), nodeOps.createElement('div'))
expect(
`was accessed during render but is not defined`
).not.toHaveBeenWarned()
})
})

View File

@@ -9,7 +9,8 @@ import {
serializeInner,
serialize,
VNodeProps,
KeepAlive
KeepAlive,
TestElement
} from '@vue/runtime-test'
function mount(
@@ -42,13 +43,13 @@ function mockProps(extra: BaseTransitionProps = {}, withKeepAlive = false) {
}
}),
onEnter: jest.fn((el, done) => {
cbs.doneEnter[serialize(el)] = done
cbs.doneEnter[serialize(el as TestElement)] = done
}),
onAfterEnter: jest.fn(),
onEnterCancelled: jest.fn(),
onBeforeLeave: jest.fn(),
onLeave: jest.fn((el, done) => {
cbs.doneLeave[serialize(el)] = done
cbs.doneLeave[serialize(el as TestElement)] = done
}),
onAfterLeave: jest.fn(),
onLeaveCancelled: jest.fn(),
@@ -64,8 +65,10 @@ function assertCalls(
props: BaseTransitionProps,
calls: Record<string, number>
) {
Object.keys(calls).forEach((key: keyof BaseTransitionProps) => {
expect(props[key]).toHaveBeenCalledTimes(calls[key])
Object.keys(calls).forEach(key => {
expect(props[key as keyof BaseTransitionProps]).toHaveBeenCalledTimes(
calls[key]
)
})
}
@@ -147,19 +150,19 @@ describe('BaseTransition', () => {
const toggle = ref(true)
const hooks: VNodeProps = {
onVnodeBeforeMount(vnode) {
vnode.transition!.beforeEnter(vnode.el)
vnode.transition!.beforeEnter(vnode.el!)
},
onVnodeMounted(vnode) {
vnode.transition!.enter(vnode.el)
vnode.transition!.enter(vnode.el!)
},
onVnodeUpdated(vnode, oldVnode) {
if (oldVnode.props!.id !== vnode.props!.id) {
if (vnode.props!.id) {
vnode.transition!.beforeEnter(vnode.el)
vnode.transition!.beforeEnter(vnode.el!)
state.show = true
vnode.transition!.enter(vnode.el)
vnode.transition!.enter(vnode.el!)
} else {
vnode.transition!.leave(vnode.el, () => {
vnode.transition!.leave(vnode.el!, () => {
state.show = false
})
}

View File

@@ -531,5 +531,32 @@ describe('KeepAlive', () => {
await nextTick()
expect(Foo.unmounted).not.toHaveBeenCalled()
})
test('should update re-activated component if props have changed', async () => {
const Foo = (props: { n: number }) => props.n
const toggle = ref(true)
const n = ref(0)
const App = {
setup() {
return () =>
h(KeepAlive, () => (toggle.value ? h(Foo, { n: n.value }) : null))
}
}
render(h(App), root)
expect(serializeInner(root)).toBe(`0`)
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
n.value++
await nextTick()
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe(`1`)
})
})
})

View File

@@ -9,6 +9,7 @@ import {
nextTick,
onMounted,
watch,
watchEffect,
onUnmounted,
onErrorCaptured
} from '@vue/runtime-test'
@@ -21,7 +22,7 @@ describe('Suspense', () => {
})
// a simple async factory for testing purposes only.
function createAsyncComponent<T extends ComponentOptions>(
function defineAsyncComponent<T extends ComponentOptions>(
comp: T,
delay: number = 0
) {
@@ -41,7 +42,7 @@ describe('Suspense', () => {
}
test('fallback content', async () => {
const Async = createAsyncComponent({
const Async = defineAsyncComponent({
render() {
return h('div', 'async')
}
@@ -69,7 +70,7 @@ describe('Suspense', () => {
test('nested async deps', async () => {
const calls: string[] = []
const AsyncOuter = createAsyncComponent({
const AsyncOuter = defineAsyncComponent({
setup() {
onMounted(() => {
calls.push('outer mounted')
@@ -78,7 +79,7 @@ describe('Suspense', () => {
}
})
const AsyncInner = createAsyncComponent(
const AsyncInner = defineAsyncComponent(
{
setup() {
onMounted(() => {
@@ -117,7 +118,7 @@ describe('Suspense', () => {
})
test('onResolve', async () => {
const Async = createAsyncComponent({
const Async = defineAsyncComponent({
render() {
return h('div', 'async')
}
@@ -163,9 +164,15 @@ describe('Suspense', () => {
// extra tick needed for Node 12+
deps.push(p.then(() => Promise.resolve()))
watch(() => {
watchEffect(() => {
calls.push('immediate effect')
})
const count = ref(0)
watch(count, v => {
calls.push('watch callback')
})
count.value++ // trigger the watcher now
onMounted(() => {
calls.push('mounted')
@@ -193,23 +200,30 @@ describe('Suspense', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
expect(calls).toEqual([`immediate effect`])
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>async</div>`)
expect(calls).toEqual([`watch callback`, `mounted`])
expect(calls).toEqual([`immediate effect`, `watch callback`, `mounted`])
// effects inside an already resolved suspense should happen at normal timing
toggle.value = false
await nextTick()
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
expect(calls).toEqual([
`immediate effect`,
`watch callback`,
`mounted`,
'unmounted'
])
})
test('content update before suspense resolve', async () => {
const Async = createAsyncComponent({
setup(props: { msg: string }) {
const Async = defineAsyncComponent({
props: { msg: String },
setup(props: any) {
return () => h('div', props.msg)
}
})
@@ -253,9 +267,15 @@ describe('Suspense', () => {
const p = new Promise(r => setTimeout(r, 1))
deps.push(p)
watch(() => {
watchEffect(() => {
calls.push('immediate effect')
})
const count = ref(0)
watch(count, () => {
calls.push('watch callback')
})
count.value++ // trigger the watcher now
onMounted(() => {
calls.push('mounted')
@@ -283,7 +303,7 @@ describe('Suspense', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
expect(calls).toEqual(['immediate effect'])
// remove the async dep before it's resolved
toggle.value = false
@@ -294,15 +314,15 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
// should discard effects
expect(calls).toEqual([])
// should discard effects (except for immediate ones)
expect(calls).toEqual(['immediate effect'])
})
test('unmount suspense after resolve', async () => {
const toggle = ref(true)
const unmounted = jest.fn()
const Async = createAsyncComponent({
const Async = defineAsyncComponent({
setup() {
onUnmounted(unmounted)
return () => h('div', 'async')
@@ -341,7 +361,7 @@ describe('Suspense', () => {
const mounted = jest.fn()
const unmounted = jest.fn()
const Async = createAsyncComponent({
const Async = defineAsyncComponent({
setup() {
onMounted(mounted)
onUnmounted(unmounted)
@@ -381,7 +401,7 @@ describe('Suspense', () => {
test('nested suspense (parent resolves first)', async () => {
const calls: string[] = []
const AsyncOuter = createAsyncComponent(
const AsyncOuter = defineAsyncComponent(
{
setup: () => {
onMounted(() => {
@@ -393,7 +413,7 @@ describe('Suspense', () => {
1
)
const AsyncInner = createAsyncComponent(
const AsyncInner = defineAsyncComponent(
{
setup: () => {
onMounted(() => {
@@ -432,14 +452,14 @@ describe('Suspense', () => {
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>async outer</div><div>fallback inner</div><!---->`
`<div>async outer</div><div>fallback inner</div>`
)
expect(calls).toEqual([`outer mounted`])
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>async outer</div><div>async inner</div><!---->`
`<div>async outer</div><div>async inner</div>`
)
expect(calls).toEqual([`outer mounted`, `inner mounted`])
})
@@ -447,7 +467,7 @@ describe('Suspense', () => {
test('nested suspense (child resolves first)', async () => {
const calls: string[] = []
const AsyncOuter = createAsyncComponent(
const AsyncOuter = defineAsyncComponent(
{
setup: () => {
onMounted(() => {
@@ -459,7 +479,7 @@ describe('Suspense', () => {
10
)
const AsyncInner = createAsyncComponent(
const AsyncInner = defineAsyncComponent(
{
setup: () => {
onMounted(() => {
@@ -503,7 +523,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>async outer</div><div>async inner</div><!---->`
`<div>async outer</div><div>async inner</div>`
)
expect(calls).toEqual([`inner mounted`, `outer mounted`])
})
@@ -517,15 +537,18 @@ describe('Suspense', () => {
const Comp = {
setup() {
const error = ref<Error | null>(null)
onErrorCaptured(e => {
error.value = e
const errorMessage = ref<string | null>(null)
onErrorCaptured(err => {
errorMessage.value =
err instanceof Error
? err.message
: `A non-Error value thrown: ${err}`
return true
})
return () =>
error.value
? h('div', error.value.message)
errorMessage.value
? h('div', errorMessage.value)
: h(Suspense, null, {
default: h(Async),
fallback: h('div', 'fallback')
@@ -546,8 +569,9 @@ describe('Suspense', () => {
const msg = ref('nested msg')
const calls: number[] = []
const AsyncChildWithSuspense = createAsyncComponent({
setup(props: { msg: string }) {
const AsyncChildWithSuspense = defineAsyncComponent({
props: { msg: String },
setup(props: any) {
onMounted(() => {
calls.push(0)
})
@@ -559,9 +583,10 @@ describe('Suspense', () => {
}
})
const AsyncInsideNestedSuspense = createAsyncComponent(
const AsyncInsideNestedSuspense = defineAsyncComponent(
{
setup(props: { msg: string }) {
props: { msg: String },
setup(props: any) {
onMounted(() => {
calls.push(2)
})
@@ -571,8 +596,9 @@ describe('Suspense', () => {
20
)
const AsyncChildParent = createAsyncComponent({
setup(props: { msg: string }) {
const AsyncChildParent = defineAsyncComponent({
props: { msg: String },
setup(props: any) {
onMounted(() => {
calls.push(1)
})
@@ -580,9 +606,10 @@ describe('Suspense', () => {
}
})
const NestedAsyncChild = createAsyncComponent(
const NestedAsyncChild = defineAsyncComponent(
{
setup(props: { msg: string }) {
props: { msg: String },
setup(props: any) {
onMounted(() => {
calls.push(3)
})
@@ -644,7 +671,7 @@ describe('Suspense', () => {
await deps[3]
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>nested fallback</div><div>root async</div><!---->`
`<div>nested fallback</div><div>root async</div>`
)
expect(calls).toEqual([0, 1, 3])
@@ -655,7 +682,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>nested changed</div><div>root async</div><!---->`
`<div>nested changed</div><div>root async</div>`
)
expect(calls).toEqual([0, 1, 3, 2])
@@ -663,20 +690,20 @@ describe('Suspense', () => {
msg.value = 'nested changed again'
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>nested changed again</div><div>root async</div><!---->`
`<div>nested changed again</div><div>root async</div>`
)
})
test('new async dep after resolve should cause suspense to restart', async () => {
const toggle = ref(false)
const ChildA = createAsyncComponent({
const ChildA = defineAsyncComponent({
setup() {
return () => h('div', 'Child A')
}
})
const ChildB = createAsyncComponent({
const ChildB = defineAsyncComponent({
setup() {
return () => h('div', 'Child B')
}
@@ -698,7 +725,7 @@ describe('Suspense', () => {
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(`<!----><div>Child A</div><!----><!---->`)
expect(serializeInner(root)).toBe(`<div>Child A</div><!---->`)
toggle.value = true
await nextTick()
@@ -706,10 +733,8 @@ describe('Suspense', () => {
await deps[1]
await nextTick()
expect(serializeInner(root)).toBe(
`<!----><div>Child A</div><div>Child B</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>Child A</div><div>Child B</div>`)
})
test.todo('portal inside suspense')
test.todo('teleport inside suspense')
})

View File

@@ -0,0 +1,302 @@
import {
nodeOps,
serializeInner,
render,
h,
Teleport,
Text,
ref,
nextTick
} from '@vue/runtime-test'
import { createVNode, Fragment } from '../../src/vnode'
describe('renderer: teleport', () => {
test('should work', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
render(
h(() => [
h(Teleport, { to: target }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
})
test('should update target', async () => {
const targetA = nodeOps.createElement('div')
const targetB = nodeOps.createElement('div')
const target = ref(targetA)
const root = nodeOps.createElement('div')
render(
h(() => [
h(Teleport, { to: target.value }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(targetA)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
expect(serializeInner(targetB)).toMatchInlineSnapshot(`""`)
target.value = targetB
await nextTick()
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(targetA)).toMatchInlineSnapshot(`""`)
expect(serializeInner(targetB)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
})
test('should update children', async () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
const children = ref([h('div', 'teleported')])
render(h(Teleport, { to: target }, children.value), root)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
children.value = []
await nextTick()
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
children.value = [createVNode(Text, null, 'teleported')]
await nextTick()
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
})
test('should remove children when unmounted', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
render(
h(() => [
h(Teleport, { to: target }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
render(null, root)
expect(serializeInner(target)).toBe('')
})
test('multiple teleport with same target', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
render(
h('div', [
h(Teleport, { to: target }, h('div', 'one')),
h(Teleport, { to: target }, 'two')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>one</div>two"`)
// update existing content
render(
h('div', [
h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
h(Teleport, { to: target }, 'three')
]),
root
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>one</div><div>two</div>three"`
)
// toggling
render(h('div', [null, h(Teleport, { to: target }, 'three')]), root)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<div><!----><!--teleport start--><!--teleport end--></div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(`"three"`)
// toggle back
render(
h('div', [
h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
h(Teleport, { to: target }, 'three')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`
)
// should append
expect(serializeInner(target)).toMatchInlineSnapshot(
`"three<div>one</div><div>two</div>"`
)
// toggle the other teleport
render(
h('div', [
h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
null
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<div><!--teleport start--><!--teleport end--><!----></div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>one</div><div>two</div>"`
)
})
test('disabled', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
const renderWithDisabled = (disabled: boolean) => {
return h(Fragment, [
h(Teleport, { to: target, disabled }, h('div', 'teleported')),
h('div', 'root')
])
}
render(renderWithDisabled(false), root)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
render(renderWithDisabled(true), root)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toBe(``)
// toggle back
render(renderWithDisabled(false), root)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
})
test('moving teleport while enabled', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
render(
h(Fragment, [
h(Teleport, { to: target }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
render(
h(Fragment, [
h('div', 'root'),
h(Teleport, { to: target }, h('div', 'teleported'))
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<div>root</div><!--teleport start--><!--teleport end-->"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
render(
h(Fragment, [
h(Teleport, { to: target }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toMatchInlineSnapshot(
`"<div>teleported</div>"`
)
})
test('moving teleport while disabled', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
render(
h(Fragment, [
h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toBe('')
render(
h(Fragment, [
h('div', 'root'),
h(Teleport, { to: target, disabled: true }, h('div', 'teleported'))
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<div>root</div><!--teleport start--><div>teleported</div><!--teleport end-->"`
)
expect(serializeInner(target)).toBe('')
render(
h(Fragment, [
h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
h('div', 'root')
]),
root
)
expect(serializeInner(root)).toMatchInlineSnapshot(
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`
)
expect(serializeInner(target)).toBe('')
})
})

View File

@@ -98,6 +98,15 @@ describe('directives', () => {
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const dir = {
beforeMount,
mounted,
beforeUpdate,
updated,
beforeUnmount,
unmounted
}
let _instance: ComponentInternalInstance | null = null
let _vnode: VNode | null = null
let _prevVnode: VNode | null = null
@@ -109,14 +118,7 @@ describe('directives', () => {
_prevVnode = _vnode
_vnode = withDirectives(h('div', count.value), [
[
{
beforeMount,
mounted,
beforeUpdate,
updated,
beforeUnmount,
unmounted
},
dir,
// value
count.value,
// argument
@@ -132,17 +134,17 @@ describe('directives', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(beforeMount).toHaveBeenCalled()
expect(mounted).toHaveBeenCalled()
expect(beforeMount).toHaveBeenCalledTimes(1)
expect(mounted).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(beforeUpdate).toHaveBeenCalled()
expect(updated).toHaveBeenCalled()
expect(beforeUpdate).toHaveBeenCalledTimes(1)
expect(updated).toHaveBeenCalledTimes(1)
render(null, root)
expect(beforeUnmount).toHaveBeenCalled()
expect(unmounted).toHaveBeenCalled()
expect(beforeUnmount).toHaveBeenCalledTimes(1)
expect(unmounted).toHaveBeenCalledTimes(1)
})
it('should work with a function directive', async () => {
@@ -198,4 +200,144 @@ describe('directives', () => {
await nextTick()
expect(fn).toHaveBeenCalledTimes(2)
})
it('should work on component vnode', async () => {
const count = ref(0)
function assertBindings(binding: DirectiveBinding) {
expect(binding.value).toBe(count.value)
expect(binding.arg).toBe('foo')
expect(binding.instance).toBe(_instance && _instance.proxy)
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
}
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should not be inserted yet
expect(el.parentNode).toBe(null)
expect(root.children.length).toBe(0)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should be inserted now
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
// node should not have been updated yet
// expect(el.children[0].text).toBe(`${count.value - 1}`)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode!.type).toBe(_prevVnode!.type)
}) as DirectiveHook)
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
// node should have been updated
expect(el.children[0].text).toBe(`${count.value}`)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode!.type).toBe(_prevVnode!.type)
}) as DirectiveHook)
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should be removed now
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should have been removed
expect(el.parentNode).toBe(null)
expect(root.children.length).toBe(0)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const dir = {
beforeMount,
mounted,
beforeUpdate,
updated,
beforeUnmount,
unmounted
}
let _instance: ComponentInternalInstance | null = null
let _vnode: VNode | null = null
let _prevVnode: VNode | null = null
const Child = (props: { count: number }) => {
_prevVnode = _vnode
_vnode = h('div', props.count)
return _vnode
}
const Comp = {
setup() {
_instance = currentInstance
},
render() {
return withDirectives(h(Child, { count: count.value }), [
[
dir,
// value
count.value,
// argument
'foo',
// modifiers
{ ok: true }
]
])
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(beforeMount).toHaveBeenCalledTimes(1)
expect(mounted).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(beforeUpdate).toHaveBeenCalledTimes(1)
expect(updated).toHaveBeenCalledTimes(1)
render(null, root)
expect(beforeUnmount).toHaveBeenCalledTimes(1)
expect(unmounted).toHaveBeenCalledTimes(1)
})
})

View File

@@ -7,10 +7,11 @@ import {
watch,
ref,
nextTick,
mockWarn,
createComponent
defineComponent,
watchEffect
} from '@vue/runtime-test'
import { setErrorRecovery } from '../src/errorHandling'
import { mockWarn } from '@vue/shared'
describe('error handling', () => {
mockWarn()
@@ -235,13 +236,13 @@ describe('error handling', () => {
}
}
const Child = createComponent(() => () => h('div', { ref }))
const Child = defineComponent(() => () => h('div', { ref }))
render(h(Comp), nodeOps.createElement('div'))
expect(fn).toHaveBeenCalledWith(err, 'ref function')
})
test('in watch (simple usage)', () => {
test('in effect', () => {
const err = new Error('foo')
const fn = jest.fn()
@@ -257,7 +258,7 @@ describe('error handling', () => {
const Child = {
setup() {
watch(() => {
watchEffect(() => {
throw err
})
return () => null
@@ -298,7 +299,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
})
test('in watch callback', () => {
test('in watch callback', async () => {
const err = new Error('foo')
const fn = jest.fn()
@@ -312,10 +313,11 @@ describe('error handling', () => {
}
}
const count = ref(0)
const Child = {
setup() {
watch(
() => 1,
() => count.value,
() => {
throw err
}
@@ -325,10 +327,13 @@ describe('error handling', () => {
}
render(h(Comp), nodeOps.createElement('div'))
count.value++
await nextTick()
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
})
test('in watch cleanup', async () => {
test('in effect cleanup', async () => {
const err = new Error('foo')
const count = ref(0)
const fn = jest.fn()
@@ -345,7 +350,7 @@ describe('error handling', () => {
const Child = {
setup() {
watch(onCleanup => {
watchEffect(onCleanup => {
count.value
onCleanup(() => {
throw err
@@ -362,7 +367,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'watcher cleanup function')
})
test('in component event handler', () => {
test('in component event handler via emit', () => {
const err = new Error('foo')
const fn = jest.fn()
@@ -392,6 +397,78 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
})
test('in component event handler via emit (async)', async () => {
const err = new Error('foo')
const fn = jest.fn()
const Comp = {
setup() {
onErrorCaptured((err, instance, info) => {
fn(err, info)
return true
})
return () =>
h(Child, {
async onFoo() {
throw err
}
})
}
}
let res: any
const Child = {
setup(props: any, { emit }: any) {
res = emit('foo')
return () => null
}
}
render(h(Comp), nodeOps.createElement('div'))
try {
await Promise.all(res)
} catch (e) {
expect(e).toBe(err)
}
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
})
test('in component event handler via emit (async + array)', async () => {
const err = new Error('foo')
const fn = jest.fn()
const Comp = {
setup() {
onErrorCaptured((err, instance, info) => {
fn(err, info)
return true
})
return () =>
h(Child, {
onFoo: [() => Promise.reject(err), () => Promise.resolve(1)]
})
}
}
let res: any
const Child = {
setup(props: any, { emit }: any) {
res = emit('foo')
return () => null
}
}
render(h(Comp), nodeOps.createElement('div'))
try {
await Promise.all(res)
} catch (e) {
expect(e).toBe(err)
}
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
})
it('should warn unhandled', () => {
const onError = jest.spyOn(console, 'error')
onError.mockImplementation(() => {})

View File

@@ -1,5 +1,4 @@
import {
mockWarn,
createApp,
nodeOps,
resolveComponent,
@@ -7,19 +6,23 @@ import {
Component,
Directive,
resolveDynamicComponent,
getCurrentInstance
h,
serializeInner,
createVNode
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
describe('resolveAssets', () => {
mockWarn()
test('should work', () => {
const app = createApp()
const FooBar = () => null
const BarBaz = { mounted: () => null }
let component1: Component
let component2: Component
let component3: Component
let component4: Component
let component1: Component | string
let component2: Component | string
let component3: Component | string
let component4: Component | string
let directive1: Directive
let directive2: Directive
let directive3: Directive
@@ -49,8 +52,9 @@ describe('resolveAssets', () => {
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(component1!).toBe(FooBar)
expect(component2!).toBe(FooBar)
expect(component3!).toBe(FooBar)
@@ -63,8 +67,6 @@ describe('resolveAssets', () => {
})
describe('warning', () => {
mockWarn()
test('used outside render() or setup()', () => {
resolveComponent('foo')
expect(
@@ -78,7 +80,6 @@ describe('resolveAssets', () => {
})
test('not exist', () => {
const app = createApp()
const Root = {
setup() {
resolveComponent('foo')
@@ -87,36 +88,64 @@ describe('resolveAssets', () => {
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect('Failed to resolve component: foo').toHaveBeenWarned()
expect('Failed to resolve directive: bar').toHaveBeenWarned()
})
test('resolve dynamic component', () => {
const app = createApp()
const dynamicComponents = {
foo: () => 'foo',
bar: () => 'bar',
baz: { render: () => 'baz' }
}
let foo, bar, baz // dynamic components
const Child = {
render(this: any) {
return this.$slots.default()
}
}
const Root = {
components: { foo: dynamicComponents.foo },
setup() {
const instance = getCurrentInstance()!
return () => {
foo = resolveDynamicComponent('foo', instance) // <component is="foo"/>
bar = resolveDynamicComponent(dynamicComponents.bar, instance) // <component :is="bar"/>, function
baz = resolveDynamicComponent(dynamicComponents.baz, instance) // <component :is="baz"/>, object
foo = resolveDynamicComponent('foo') // <component is="foo"/>
bar = resolveDynamicComponent(dynamicComponents.bar) // <component :is="bar"/>, function
return h(Child, () => {
// check inside child slots
baz = resolveDynamicComponent(dynamicComponents.baz) // <component :is="baz"/>, object
})
}
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(foo).toBe(dynamicComponents.foo)
expect(bar).toBe(dynamicComponents.bar)
expect(baz).toBe(dynamicComponents.baz)
})
test('resolve dynamic component should fallback to plain element without warning', () => {
const Root = {
setup() {
return () => {
return createVNode(resolveDynamicComponent('div') as string, null, {
default: () => 'hello'
})
}
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(root)
expect(serializeInner(root)).toBe('<div>hello</div>')
})
})
})

View File

@@ -0,0 +1,68 @@
import { withScopeId } from '../../src/helpers/scopeId'
import { h, render, nodeOps, serializeInner } from '@vue/runtime-test'
describe('scopeId runtime support', () => {
const withParentId = withScopeId('parent')
const withChildId = withScopeId('child')
test('should attach scopeId', () => {
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h('div', [h('div')])
})
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div parent><div parent></div></div>`)
})
test('should attach scopeId to components in parent component', () => {
const Child = {
__scopeId: 'child',
render: withChildId(() => {
return h('div')
})
}
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h('div', [h(Child)])
})
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(
`<div parent><div parent child></div></div>`
)
})
test('should work on slots', () => {
const Child = {
__scopeId: 'child',
render: withChildId(function(this: any) {
return h('div', this.$slots.default())
})
}
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h(
Child,
withParentId(() => {
return h('div')
})
)
})
}
const root = nodeOps.createElement('div')
render(h(App), root)
// slot content should have:
// - scopeId from parent
// - slotted scopeId (with `-s` postfix) from child (the tree owner)
expect(serializeInner(root)).toBe(
`<div parent child><div parent child-s></div></div>`
)
})
})

View File

@@ -1,5 +1,5 @@
import { toHandlers } from '../../src/helpers/toHandlers'
import { mockWarn } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
describe('toHandlers', () => {
mockWarn()

View File

@@ -0,0 +1,148 @@
import { HMRRuntime } from '../src/hmr'
import '../src/hmr'
import { ComponentOptions, RenderFunction } from '../src/component'
import {
render,
nodeOps,
h,
serializeInner,
triggerEvent,
TestElement,
nextTick
} from '@vue/runtime-test'
import * as runtimeTest from '@vue/runtime-test'
import { baseCompile } from '@vue/compiler-core'
declare var __VUE_HMR_RUNTIME__: HMRRuntime
const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
function compileToFunction(template: string) {
const { code } = baseCompile(template)
const render = new Function('Vue', code)(runtimeTest) as RenderFunction
render._rc = true // isRuntimeCompiled
return render
}
describe('hot module replacement', () => {
test('inject global runtime', () => {
expect(createRecord).toBeDefined()
expect(rerender).toBeDefined()
expect(reload).toBeDefined()
})
test('createRecord', () => {
expect(createRecord('test1', {})).toBe(true)
// if id has already been created, should return false
expect(createRecord('test1', {})).toBe(false)
})
test('rerender', async () => {
const root = nodeOps.createElement('div')
const parentId = 'test2-parent'
const childId = 'test2-child'
const Child: ComponentOptions = {
__hmrId: childId,
render: compileToFunction(`<slot/>`)
}
createRecord(childId, Child)
const Parent: ComponentOptions = {
__hmrId: parentId,
data() {
return { count: 0 }
},
components: { Child },
render: compileToFunction(
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
)
}
createRecord(parentId, Parent)
render(h(Parent), root)
expect(serializeInner(root)).toBe(`<div>00</div>`)
// Perform some state change. This change should be preserved after the
// re-render!
triggerEvent(root.children[0] as TestElement, 'click')
await nextTick()
expect(serializeInner(root)).toBe(`<div>11</div>`)
// // Update text while preserving state
// rerender(
// parentId,
// compileToFunction(
// `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
// )
// )
// expect(serializeInner(root)).toBe(`<div>1!1</div>`)
// Should force child update on slot content change
rerender(
parentId,
compileToFunction(
`<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`
)
)
expect(serializeInner(root)).toBe(`<div>1!1!</div>`)
// Should force update element children despite block optimization
rerender(
parentId,
compileToFunction(
`<div @click="count++">{{ count }}<span>{{ count }}</span>
<Child>{{ count }}!</Child>
</div>`
)
)
expect(serializeInner(root)).toBe(`<div>1<span>1</span>1!</div>`)
// Should force update child slot elements
rerender(
parentId,
compileToFunction(
`<div @click="count++">
<Child><span>{{ count }}</span></Child>
</div>`
)
)
expect(serializeInner(root)).toBe(`<div><span>1</span></div>`)
})
test('reload', async () => {
const root = nodeOps.createElement('div')
const childId = 'test3-child'
const unmoutSpy = jest.fn()
const mountSpy = jest.fn()
const Child: ComponentOptions = {
__hmrId: childId,
data() {
return { count: 0 }
},
unmounted: unmoutSpy,
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
}
createRecord(childId, Child)
const Parent: ComponentOptions = {
render: () => h(Child)
}
render(h(Parent), root)
expect(serializeInner(root)).toBe(`<div>0</div>`)
reload(childId, {
__hmrId: childId,
data() {
return { count: 1 }
},
mounted: mountSpy,
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
})
await nextTick()
expect(serializeInner(root)).toBe(`<div>1</div>`)
expect(unmoutSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,660 @@
import {
createSSRApp,
h,
ref,
nextTick,
VNode,
Teleport,
createStaticVNode,
Suspense,
onMounted,
defineAsyncComponent,
defineComponent
} from '@vue/runtime-dom'
import { renderToString } from '@vue/server-renderer'
import { mockWarn } from '@vue/shared'
import { SSRContext } from 'packages/server-renderer/src/renderToString'
function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div')
container.innerHTML = html
const app = createSSRApp({
render
})
return {
vnode: app.mount(container).$.subTree as VNode<Node, Element> & {
el: Element
},
container
}
}
const triggerEvent = (type: string, el: Element) => {
const event = new Event(type)
el.dispatchEvent(event)
}
describe('SSR hydration', () => {
mockWarn()
test('text', async () => {
const msg = ref('foo')
const { vnode, container } = mountWithHydration('foo', () => msg.value)
expect(vnode.el).toBe(container.firstChild)
expect(container.textContent).toBe('foo')
msg.value = 'bar'
await nextTick()
expect(container.textContent).toBe('bar')
})
test('comment', () => {
const { vnode, container } = mountWithHydration('<!---->', () => null)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.el.nodeType).toBe(8) // comment
})
test('static', () => {
const html = '<div><span>hello</span></div>'
const { vnode, container } = mountWithHydration(html, () =>
createStaticVNode(html)
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.el.outerHTML).toBe(html)
})
test('element with text children', async () => {
const msg = ref('foo')
const { vnode, container } = mountWithHydration(
'<div class="foo">foo</div>',
() => h('div', { class: msg.value }, msg.value)
)
expect(vnode.el).toBe(container.firstChild)
expect(container.firstChild!.textContent).toBe('foo')
msg.value = 'bar'
await nextTick()
expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
})
test('element with elements children', async () => {
const msg = ref('foo')
const fn = jest.fn()
const { vnode, container } = mountWithHydration(
'<div><span>foo</span><span class="foo"></span></div>',
() =>
h('div', [
h('span', msg.value),
h('span', { class: msg.value, onClick: fn })
])
)
expect(vnode.el).toBe(container.firstChild)
expect((vnode.children as VNode[])[0].el).toBe(
container.firstChild!.childNodes[0]
)
expect((vnode.children as VNode[])[1].el).toBe(
container.firstChild!.childNodes[1]
)
// event handler
triggerEvent('click', vnode.el.querySelector('.foo')!)
expect(fn).toHaveBeenCalled()
msg.value = 'bar'
await nextTick()
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
})
test('Fragment', async () => {
const msg = ref('foo')
const fn = jest.fn()
const { vnode, container } = mountWithHydration(
'<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
() =>
h('div', [
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
])
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.el.innerHTML).toBe(
`<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
)
// start fragment 1
const fragment1 = (vnode.children as VNode[])[0]
expect(fragment1.el).toBe(vnode.el.childNodes[0])
const fragment1Children = fragment1.children as VNode[]
// first <span>
expect(fragment1Children[0].el!.tagName).toBe('SPAN')
expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
// start fragment 2
const fragment2 = fragment1Children[1]
expect(fragment2.el).toBe(vnode.el.childNodes[2])
const fragment2Children = fragment2.children as VNode[]
// second <span>
expect(fragment2Children[0].el!.tagName).toBe('SPAN')
expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
// end fragment 2
expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
// end fragment 1
expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
// event handler
triggerEvent('click', vnode.el.querySelector('.foo')!)
expect(fn).toHaveBeenCalled()
msg.value = 'bar'
await nextTick()
expect(vnode.el.innerHTML).toBe(
`<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`
)
})
test('Teleport', async () => {
const msg = ref('foo')
const fn = jest.fn()
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!---->`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
'<!--teleport start--><!--teleport end-->',
() =>
h(Teleport, { to: '#teleport' }, [
h('span', msg.value),
h('span', { class: msg.value, onClick: fn })
])
)
expect(vnode.el).toBe(container.firstChild)
expect(vnode.anchor).toBe(container.lastChild)
expect(vnode.target).toBe(teleportContainer)
expect((vnode.children as VNode[])[0].el).toBe(
teleportContainer.childNodes[0]
)
expect((vnode.children as VNode[])[1].el).toBe(
teleportContainer.childNodes[1]
)
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
expect(fn).toHaveBeenCalled()
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
`<span>bar</span><span class="bar"></span><!---->`
)
})
test('Teleport (multiple + integration)', async () => {
const msg = ref('foo')
const fn1 = jest.fn()
const fn2 = jest.fn()
const Comp = () => [
h(Teleport, { to: '#teleport2' }, [
h('span', msg.value),
h('span', { class: msg.value, onClick: fn1 })
]),
h(Teleport, { to: '#teleport2' }, [
h('span', msg.value + '2'),
h('span', { class: msg.value + '2', onClick: fn2 })
])
]
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport2'
const ctx: SSRContext = {}
const mainHtml = await renderToString(h(Comp), ctx)
expect(mainHtml).toMatchInlineSnapshot(
`"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`
)
const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot(
`"<span>foo</span><span class=\\"foo\\"></span><!----><span>foo2</span><span class=\\"foo2\\"></span><!---->"`
)
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(mainHtml, Comp)
expect(vnode.el).toBe(container.firstChild)
const teleportVnode1 = (vnode.children as VNode[])[0]
const teleportVnode2 = (vnode.children as VNode[])[1]
expect(teleportVnode1.el).toBe(container.childNodes[1])
expect(teleportVnode1.anchor).toBe(container.childNodes[2])
expect(teleportVnode2.el).toBe(container.childNodes[3])
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
expect(teleportVnode1.target).toBe(teleportContainer)
expect((teleportVnode1 as any).children[0].el).toBe(
teleportContainer.childNodes[0]
)
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
expect(teleportVnode2.target).toBe(teleportContainer)
expect((teleportVnode2 as any).children[0].el).toBe(
teleportContainer.childNodes[3]
)
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
// // event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
expect(fn1).toHaveBeenCalled()
triggerEvent('click', teleportContainer.querySelector('.foo2')!)
expect(fn2).toHaveBeenCalled()
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
`"<span>bar</span><span class=\\"bar\\"></span><!----><span>bar2</span><span class=\\"bar2\\"></span><!---->"`
)
})
test('Teleport (disabled)', async () => {
const msg = ref('foo')
const fn1 = jest.fn()
const fn2 = jest.fn()
const Comp = () => [
h('div', 'foo'),
h(Teleport, { to: '#teleport3', disabled: true }, [
h('span', msg.value),
h('span', { class: msg.value, onClick: fn1 })
]),
h('div', { class: msg.value + '2', onClick: fn2 }, 'bar')
]
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport3'
const ctx: SSRContext = {}
const mainHtml = await renderToString(h(Comp), ctx)
expect(mainHtml).toMatchInlineSnapshot(
`"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class=\\"foo\\"></span><!--teleport end--><div class=\\"foo2\\">bar</div><!--]-->"`
)
const teleportHtml = ctx.teleports!['#teleport3']
expect(teleportHtml).toMatchInlineSnapshot(`"<!---->"`)
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(mainHtml, Comp)
expect(vnode.el).toBe(container.firstChild)
const children = vnode.children as VNode[]
expect(children[0].el).toBe(container.childNodes[1])
const teleportVnode = children[1]
expect(teleportVnode.el).toBe(container.childNodes[2])
expect((teleportVnode.children as VNode[])[0].el).toBe(
container.childNodes[3]
)
expect((teleportVnode.children as VNode[])[1].el).toBe(
container.childNodes[4]
)
expect(teleportVnode.anchor).toBe(container.childNodes[5])
expect(children[2].el).toBe(container.childNodes[6])
expect(teleportVnode.target).toBe(teleportContainer)
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
// // event handler
triggerEvent('click', container.querySelector('.foo')!)
expect(fn1).toHaveBeenCalled()
triggerEvent('click', container.querySelector('.foo2')!)
expect(fn2).toHaveBeenCalled()
msg.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class=\\"bar\\"></span><!--teleport end--><div class=\\"bar2\\">bar</div><!--]-->"`
)
})
// compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => {
const mounted: string[] = []
const log = jest.fn()
const toggle = ref(true)
const Child = {
data() {
return {
count: 0,
text: 'hello',
style: {
color: 'red'
}
}
},
mounted() {
mounted.push('child')
},
template: `
<div>
<span class="count" :style="style">{{ count }}</span>
<button class="inc" @click="count++">inc</button>
<button class="change" @click="style.color = 'green'" >change color</button>
<button class="emit" @click="$emit('foo')">emit</button>
<span class="text">{{ text }}</span>
<input v-model="text">
</div>
`
}
const App = {
setup() {
return { toggle }
},
mounted() {
mounted.push('parent')
},
template: `
<div>
<span>hello</span>
<template v-if="toggle">
<Child @foo="log('child')"/>
<template v-if="true">
<button class="parent-click" @click="log('click')">click me</button>
</template>
</template>
<span>hello</span>
</div>`,
components: {
Child
},
methods: {
log
}
}
const container = document.createElement('div')
// server render
container.innerHTML = await renderToString(h(App))
// hydrate
createSSRApp(App).mount(container)
// assert interactions
// 1. parent button click
triggerEvent('click', container.querySelector('.parent-click')!)
expect(log).toHaveBeenCalledWith('click')
// 2. child inc click + text interpolation
const count = container.querySelector('.count') as HTMLElement
expect(count.textContent).toBe(`0`)
triggerEvent('click', container.querySelector('.inc')!)
await nextTick()
expect(count.textContent).toBe(`1`)
// 3. child color click + style binding
expect(count.style.color).toBe('red')
triggerEvent('click', container.querySelector('.change')!)
await nextTick()
expect(count.style.color).toBe('green')
// 4. child event emit
triggerEvent('click', container.querySelector('.emit')!)
expect(log).toHaveBeenCalledWith('child')
// 5. child v-model
const text = container.querySelector('.text')!
const input = container.querySelector('input')!
expect(text.textContent).toBe('hello')
input.value = 'bye'
triggerEvent('input', input)
await nextTick()
expect(text.textContent).toBe('bye')
})
test('Suspense', async () => {
const AsyncChild = {
async setup() {
const count = ref(0)
return () =>
h(
'span',
{
onClick: () => {
count.value++
}
},
count.value
)
}
}
const { vnode, container } = mountWithHydration('<span>0</span>', () =>
h(Suspense, () => h(AsyncChild))
)
expect(vnode.el).toBe(container.firstChild)
// wait for hydration to finish
await new Promise(r => setTimeout(r))
triggerEvent('click', container.querySelector('span')!)
await nextTick()
expect(container.innerHTML).toBe(`<span>1</span>`)
})
test('Suspense (full integration)', async () => {
const mountedCalls: number[] = []
const asyncDeps: Promise<any>[] = []
const AsyncChild = defineComponent({
props: ['n'],
async setup(props) {
const count = ref(props.n)
onMounted(() => {
mountedCalls.push(props.n)
})
const p = new Promise(r => setTimeout(r, props.n * 10))
asyncDeps.push(p)
await p
return () =>
h(
'span',
{
onClick: () => {
count.value++
}
},
count.value
)
}
})
const done = jest.fn()
const App = {
template: `
<Suspense @resolve="done">
<AsyncChild :n="1" />
<AsyncChild :n="2" />
</Suspense>`,
components: {
AsyncChild
},
methods: {
done
}
}
const container = document.createElement('div')
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--[--><span>1</span><span>2</span><!--]-->"`
)
// reset asyncDeps from ssr
asyncDeps.length = 0
// hydrate
createSSRApp(App).mount(container)
expect(mountedCalls.length).toBe(0)
expect(asyncDeps.length).toBe(2)
// wait for hydration to complete
await Promise.all(asyncDeps)
await new Promise(r => setTimeout(r))
// should flush buffered effects
expect(mountedCalls).toMatchObject([1, 2])
expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
const span1 = container.querySelector('span')!
triggerEvent('click', span1)
await nextTick()
expect(container.innerHTML).toMatch(`<span>2</span><span>2</span>`)
const span2 = span1.nextSibling as Element
triggerEvent('click', span2)
await nextTick()
expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
})
test('async component', async () => {
const spy = jest.fn()
const Comp = () =>
h(
'button',
{
onClick: spy
},
'hello!'
)
let serverResolve: any
let AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
serverResolve = r
})
)
const App = {
render() {
return ['hello', h(AsyncComp), 'world']
}
}
// server render
const htmlPromise = renderToString(h(App))
serverResolve(Comp)
const html = await htmlPromise
expect(html).toMatchInlineSnapshot(
`"<!--[-->hello<button>hello!</button>world<!--]-->"`
)
// hydration
let clientResolve: any
AsyncComp = defineAsyncComponent(
() =>
new Promise(r => {
clientResolve = r
})
)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
// hydration not complete yet
triggerEvent('click', container.querySelector('button')!)
expect(spy).not.toHaveBeenCalled()
// resolve
clientResolve(Comp)
await new Promise(r => setTimeout(r))
// should be hydrated now
triggerEvent('click', container.querySelector('button')!)
expect(spy).toHaveBeenCalled()
})
describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
expect(container.textContent).toBe('bar')
expect(`Hydration text mismatch`).toHaveBeenWarned()
})
test('element text content', () => {
const { container } = mountWithHydration(`<div>foo</div>`, () =>
h('div', 'bar')
)
expect(container.innerHTML).toBe('<div>bar</div>')
expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
})
test('not enough children', () => {
const { container } = mountWithHydration(`<div></div>`, () =>
h('div', [h('span', 'foo'), h('span', 'bar')])
)
expect(container.innerHTML).toBe(
'<div><span>foo</span><span>bar</span></div>'
)
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
})
test('too many children', () => {
const { container } = mountWithHydration(
`<div><span>foo</span><span>bar</span></div>`,
() => h('div', [h('span', 'foo')])
)
expect(container.innerHTML).toBe('<div><span>foo</span></div>')
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
})
test('complete mismatch', () => {
const { container } = mountWithHydration(
`<div><span>foo</span><span>bar</span></div>`,
() => h('div', [h('div', 'foo'), h('p', 'bar')])
)
expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
})
test('fragment mismatch removal', () => {
const { container } = mountWithHydration(
`<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
() => h('div', [h('span', 'replaced')])
)
expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
test('fragment not enough children', () => {
const { container } = mountWithHydration(
`<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
() => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
)
expect(container.innerHTML).toBe(
'<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
)
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
test('fragment too many children', () => {
const { container } = mountWithHydration(
`<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
() => h('div', [[h('div', 'foo')], h('div', 'baz')])
)
expect(container.innerHTML).toBe(
'<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
)
// fragment ends early and attempts to hydrate the extra <div>bar</div>
// as 2nd fragment child.
expect(`Hydration text content mismatch`).toHaveBeenWarned()
// exccesive children removal
expect(`Hydration children mismatch`).toHaveBeenWarned()
})
})
})

View File

@@ -6,14 +6,18 @@ import {
mergeProps,
ref,
onUpdated,
createComponent
defineComponent,
openBlock,
createBlock,
FunctionalComponent,
createCommentVNode
} from '@vue/runtime-dom'
import { mockWarn } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
describe('attribute fallthrough', () => {
mockWarn()
it('everything should be in props when component has no declared props', async () => {
it('should allow attrs to fallthrough', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
@@ -28,85 +32,18 @@ describe('attribute fallthrough', () => {
return () =>
h(Child, {
foo: 1,
foo: count.value + 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
onClick: inc,
'data-id': count.value + 1
})
}
}
const Child = {
setup(props: any) {
onUpdated(childUpdated)
return () =>
h(
'div',
mergeProps(
{
class: 'c2',
style: { fontWeight: 'bold' }
},
props
),
props.foo
)
}
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
const node = root.children[0] as HTMLElement
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe('1')
expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold')
node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe('1')
expect(node.getAttribute('class')).toBe('c2 c1')
expect(node.style.color).toBe('red')
expect(node.style.fontWeight).toBe('bold')
})
it('should implicitly fallthrough on single root nodes', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const Hello = {
setup() {
const count = ref(0)
function inc() {
count.value++
click()
}
return () =>
h(Child, {
foo: 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
}
}
const Child = createComponent({
props: {
foo: Number
},
setup(props) {
onUpdated(childUpdated)
return () =>
h(
@@ -118,7 +55,7 @@ describe('attribute fallthrough', () => {
props.foo
)
}
})
}
const root = document.createElement('div')
document.body.appendChild(root)
@@ -126,25 +63,137 @@ describe('attribute fallthrough', () => {
const node = root.children[0] as HTMLElement
// with declared props, any parent attr that isn't a prop falls through
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe('1')
expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold')
expect(node.dataset.id).toBe('1')
node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe('2')
expect(node.getAttribute('class')).toBe('c2 c1')
expect(node.style.color).toBe('red')
expect(node.style.fontWeight).toBe('bold')
expect(node.dataset.id).toBe('2')
})
it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const count = ref(0)
function inc() {
count.value++
click()
}
const Hello = () =>
h(Child, {
foo: count.value + 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
const Child = (props: any) => {
childUpdated()
return h(
'div',
{
class: 'c2',
style: { fontWeight: 'bold' }
},
props.foo
)
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
const node = root.children[0] as HTMLElement
// not whitelisted
expect(node.getAttribute('id')).toBe(null)
expect(node.getAttribute('foo')).toBe(null)
// whitelisted: style, class, event listeners
expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold')
node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
// ...while declared ones remain props
expect(node.hasAttribute('foo')).toBe(false)
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe(null)
expect(node.getAttribute('foo')).toBe(null)
expect(node.getAttribute('class')).toBe('c2 c1')
expect(node.style.color).toBe('red')
expect(node.style.fontWeight).toBe('bold')
})
it('should allow all attrs on functional component with declared props', async () => {
const click = jest.fn()
const childUpdated = jest.fn()
const count = ref(0)
function inc() {
count.value++
click()
}
const Hello = () =>
h(Child, {
foo: count.value + 1,
id: 'test',
class: 'c' + count.value,
style: { color: count.value ? 'red' : 'green' },
onClick: inc
})
const Child = (props: { foo: number }) => {
childUpdated()
return h(
'div',
{
class: 'c2',
style: { fontWeight: 'bold' }
},
props.foo
)
}
Child.props = ['foo']
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
const node = root.children[0] as HTMLElement
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe(null) // declared as prop
expect(node.getAttribute('class')).toBe('c2 c0')
expect(node.style.color).toBe('green')
expect(node.style.fontWeight).toBe('bold')
node.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
await nextTick()
expect(childUpdated).toHaveBeenCalled()
expect(node.getAttribute('id')).toBe('test')
expect(node.getAttribute('foo')).toBe(null)
expect(node.getAttribute('class')).toBe('c2 c1')
expect(node.style.color).toBe('red')
expect(node.style.fontWeight).toBe('bold')
expect(node.hasAttribute('foo')).toBe(false)
})
it('should fallthrough for nested components', async () => {
@@ -175,12 +224,16 @@ describe('attribute fallthrough', () => {
const Child = {
setup(props: any) {
onUpdated(childUpdated)
// HOC simply passing props down.
// this will result in merging the same attrs, but should be deduped by
// `mergeProps`.
return () => h(GrandChild, props)
}
}
const GrandChild = createComponent({
const GrandChild = defineComponent({
props: {
id: String,
foo: Number
},
setup(props) {
@@ -189,6 +242,7 @@ describe('attribute fallthrough', () => {
h(
'div',
{
id: props.id,
class: 'c2',
style: { fontWeight: 'bold' }
},
@@ -232,7 +286,7 @@ describe('attribute fallthrough', () => {
}
}
const Child = createComponent({
const Child = defineComponent({
props: ['foo'],
inheritAttrs: false,
render() {
@@ -255,7 +309,7 @@ describe('attribute fallthrough', () => {
}
}
const Child = createComponent({
const Child = defineComponent({
props: ['foo'],
inheritAttrs: false,
render() {
@@ -287,7 +341,7 @@ describe('attribute fallthrough', () => {
}
}
const Child = createComponent({
const Child = defineComponent({
props: ['foo'],
render() {
return [h('div'), h('div')]
@@ -308,7 +362,7 @@ describe('attribute fallthrough', () => {
}
}
const Child = createComponent({
const Child = defineComponent({
props: ['foo'],
render() {
return [h('div'), h('div', this.$attrs)]
@@ -320,8 +374,157 @@ describe('attribute fallthrough', () => {
render(h(Parent), root)
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
})
it('should not warn when context.attrs is used during render', () => {
const Parent = {
render() {
return h(Child, { foo: 1, class: 'parent' })
}
}
const Child = defineComponent({
props: ['foo'],
setup(_props, { attrs }) {
return () => [h('div'), h('div', attrs)]
}
})
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Parent), root)
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
})
// #677
it('should update merged dynamic attrs on optimized child root', async () => {
const aria = ref('true')
const cls = ref('bar')
const Parent = {
render() {
return h(Child, { 'aria-hidden': aria.value, class: cls.value })
}
}
const Child = {
props: [],
render() {
return openBlock(), createBlock('div')
}
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Parent), root)
expect(root.innerHTML).toBe(`<div aria-hidden="true" class="bar"></div>`)
aria.value = 'false'
await nextTick()
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="bar"></div>`)
cls.value = 'barr'
await nextTick()
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
})
it('should not let listener fallthrough when declared in emits (stateful)', () => {
const Child = defineComponent({
emits: ['click'],
render() {
return h(
'button',
{
onClick: () => {
this.$emit('click', 'custom')
}
},
'hello'
)
}
})
const onClick = jest.fn()
const App = {
render() {
return h(Child, {
onClick
})
}
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(App), root)
const node = root.children[0] as HTMLElement
node.dispatchEvent(new CustomEvent('click'))
expect(onClick).toHaveBeenCalledTimes(1)
expect(onClick).toHaveBeenCalledWith('custom')
})
it('should not let listener fallthrough when declared in emits (functional)', () => {
const Child: FunctionalComponent<{}, { click: any }> = (_, { emit }) => {
// should not be in props
expect((_ as any).onClick).toBeUndefined()
return h('button', {
onClick: () => {
emit('click', 'custom')
}
})
}
Child.emits = ['click']
const onClick = jest.fn()
const App = {
render() {
return h(Child, {
onClick
})
}
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(App), root)
const node = root.children[0] as HTMLElement
node.dispatchEvent(new CustomEvent('click'))
expect(onClick).toHaveBeenCalledTimes(1)
expect(onClick).toHaveBeenCalledWith('custom')
})
it('should support fallthrough for fragments with single element + comments', () => {
const click = jest.fn()
const Hello = {
setup() {
return () => h(Child, { class: 'foo', onClick: click })
}
}
const Child = {
setup(props: any) {
return () => [
createCommentVNode('hello'),
h('button'),
createCommentVNode('world')
]
}
}
const root = document.createElement('div')
document.body.appendChild(root)
render(h(Hello), root)
expect(root.innerHTML).toBe(
`<!----><div></div><div class="parent"></div><!---->`
`<!--hello--><button class="foo"></button><!--world-->`
)
const button = root.children[0] as HTMLElement
button.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
})
})

View File

@@ -6,9 +6,9 @@ import {
NodeTypes,
TestElement,
serialize,
serializeInner,
mockWarn
serializeInner
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
mockWarn()

View File

@@ -1,9 +1,51 @@
import {
h,
render,
nodeOps,
TestElement,
serializeInner as inner
} from '@vue/runtime-test'
describe('renderer: element', () => {
test.todo('with props')
let root: TestElement
test.todo('with direct text children')
beforeEach(() => {
root = nodeOps.createElement('div')
})
test.todo('with text node children')
it('should create an element', () => {
render(h('div'), root)
expect(inner(root)).toBe('<div></div>')
})
test.todo('handle already mounted VNode')
it('should create an element with props', () => {
render(h('div', { id: 'foo', class: 'bar' }), root)
expect(inner(root)).toBe('<div id="foo" class="bar"></div>')
})
it('should create an element with direct text children', () => {
render(h('div', ['foo', ' ', 'bar']), root)
expect(inner(root)).toBe('<div>foo bar</div>')
})
it('should create an element with direct text children and props', () => {
render(h('div', { id: 'foo' }, ['bar']), root)
expect(inner(root)).toBe('<div id="foo">bar</div>')
})
it('should update an element tag which is already mounted', () => {
render(h('div', ['foo']), root)
expect(inner(root)).toBe('<div>foo</div>')
render(h('span', ['foo']), root)
expect(inner(root)).toBe('<span>foo</span>')
})
it('should update element props which is already mounted', () => {
render(h('div', { id: 'bar' }, ['foo']), root)
expect(inner(root)).toBe('<div id="bar">foo</div>')
render(h('div', { id: 'baz', class: 'bar' }, ['foo']), root)
expect(inner(root)).toBe('<div id="baz" class="bar">foo</div>')
})
})

View File

@@ -6,13 +6,13 @@ import {
NodeTypes,
TestElement,
Fragment,
PatchFlags,
resetOps,
dumpOps,
NodeOpTypes,
serializeInner,
createTextVNode
} from '@vue/runtime-test'
import { PatchFlags } from '@vue/shared'
describe('renderer: fragment', () => {
it('should allow returning multiple component root nodes', () => {
@@ -25,10 +25,11 @@ describe('renderer: fragment', () => {
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`)
expect(serializeInner(root)).toBe(`<div>one</div>two`)
expect(root.children.length).toBe(4)
expect(root.children[0]).toMatchObject({
type: NodeTypes.COMMENT
type: NodeTypes.TEXT,
text: ''
})
expect(root.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
@@ -43,7 +44,8 @@ describe('renderer: fragment', () => {
text: 'two'
})
expect(root.children[3]).toMatchObject({
type: NodeTypes.COMMENT
type: NodeTypes.TEXT,
text: ''
})
})
@@ -51,7 +53,7 @@ describe('renderer: fragment', () => {
const root = nodeOps.createElement('div')
render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root)
const parent = root.children[0] as TestElement
expect(serializeInner(parent)).toBe(`<!----><div>one</div>two<!---->`)
expect(serializeInner(parent)).toBe(`<div>one</div>two`)
})
it('patch fragment children (manual, keyed)', () => {
@@ -60,18 +62,14 @@ describe('renderer: fragment', () => {
h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]),
root
)
expect(serializeInner(root)).toBe(
`<!----><div>one</div><div>two</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
resetOps()
render(
h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]),
root
)
expect(serializeInner(root)).toBe(
`<!----><div>two</div><div>one</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
const ops = dumpOps()
// should be moving nodes instead of re-creating or patching them
expect(ops).toMatchObject([
@@ -84,15 +82,11 @@ describe('renderer: fragment', () => {
it('patch fragment children (manual, unkeyed)', () => {
const root = nodeOps.createElement('div')
render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root)
expect(serializeInner(root)).toBe(
`<!----><div>one</div><div>two</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
resetOps()
render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root)
expect(serializeInner(root)).toBe(
`<!----><div>two</div><div>one</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
const ops = dumpOps()
// should be patching nodes instead of moving or re-creating them
expect(ops).toMatchObject([
@@ -119,7 +113,7 @@ describe('renderer: fragment', () => {
),
root
)
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`)
expect(serializeInner(root)).toBe(`<div>one</div>two`)
render(
createVNode(
@@ -134,7 +128,7 @@ describe('renderer: fragment', () => {
),
root
)
expect(serializeInner(root)).toBe(`<!----><div>foo</div>barbaz<!---->`)
expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
})
it('patch fragment children (compiler generated, keyed)', () => {
@@ -149,9 +143,7 @@ describe('renderer: fragment', () => {
),
root
)
expect(serializeInner(root)).toBe(
`<!----><div>one</div><div>two</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
resetOps()
render(
@@ -163,9 +155,7 @@ describe('renderer: fragment', () => {
),
root
)
expect(serializeInner(root)).toBe(
`<!----><div>two</div><div>one</div><!---->`
)
expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
const ops = dumpOps()
// should be moving nodes instead of re-creating or patching them
expect(ops).toMatchObject([
@@ -188,7 +178,7 @@ describe('renderer: fragment', () => {
root
)
expect(serializeInner(root)).toBe(
`<div><div>outer</div><!----><div>one</div><div>two</div><!----></div>`
`<div><div>outer</div><div>one</div><div>two</div></div>`
)
resetOps()
@@ -203,7 +193,7 @@ describe('renderer: fragment', () => {
root
)
expect(serializeInner(root)).toBe(
`<div><!----><div>two</div><div>one</div><!----><div>outer</div></div>`
`<div><div>two</div><div>one</div><div>outer</div></div>`
)
const ops = dumpOps()
// should be moving nodes instead of re-creating them
@@ -213,10 +203,10 @@ describe('renderer: fragment', () => {
// 2. move entire fragment, including anchors
// not the most efficient move, but this case is super rare
// and optimizing for this special case complicates the algo quite a bit
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
])
})
@@ -234,7 +224,7 @@ describe('renderer: fragment', () => {
root
)
expect(serializeInner(root)).toBe(
`<!----><div>outer</div><!----><div>one</div><div>two</div><!----><!---->`
`<div>outer</div><div>one</div><div>two</div>`
)
resetOps()
@@ -249,16 +239,16 @@ describe('renderer: fragment', () => {
root
)
expect(serializeInner(root)).toBe(
`<!----><!----><div>two</div><div>one</div><!----><div>outer</div><!---->`
`<div>two</div><div>one</div><div>outer</div>`
)
const ops = dumpOps()
// should be moving nodes instead of re-creating them
expect(ops).toMatchObject([
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
])
// should properly remove nested fragments

View File

@@ -1,84 +0,0 @@
import {
nodeOps,
serializeInner,
render,
h,
createComponent,
Portal,
Text,
Fragment,
ref,
nextTick,
TestElement,
TestNode
} from '@vue/runtime-test'
import { VNodeChildren } from '../src/vnode'
describe('renderer: portal', () => {
test('should work', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
const Comp = createComponent(() => () =>
h(Fragment, [
h(Portal, { target }, h('div', 'teleported')),
h('div', 'root')
])
)
render(h(Comp), root)
expect(serializeInner(root)).toMatchSnapshot()
expect(serializeInner(target)).toMatchSnapshot()
})
test('should update target', async () => {
const targetA = nodeOps.createElement('div')
const targetB = nodeOps.createElement('div')
const target = ref(targetA)
const root = nodeOps.createElement('div')
const Comp = createComponent(() => () =>
h(Fragment, [
h(Portal, { target: target.value }, h('div', 'teleported')),
h('div', 'root')
])
)
render(h(Comp), root)
expect(serializeInner(root)).toMatchSnapshot()
expect(serializeInner(targetA)).toMatchSnapshot()
expect(serializeInner(targetB)).toMatchSnapshot()
target.value = targetB
await nextTick()
expect(serializeInner(root)).toMatchSnapshot()
expect(serializeInner(targetA)).toMatchSnapshot()
expect(serializeInner(targetB)).toMatchSnapshot()
})
test('should update children', async () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
const children = ref<VNodeChildren<TestNode, TestElement>>([
h('div', 'teleported')
])
const Comp = createComponent(() => () =>
h(Portal, { target }, children.value)
)
render(h(Comp), root)
expect(serializeInner(target)).toMatchSnapshot()
children.value = []
await nextTick()
expect(serializeInner(target)).toMatchSnapshot()
children.value = [h(Text, 'teleported')]
await nextTick()
expect(serializeInner(target)).toMatchSnapshot()
})
})

View File

@@ -1,4 +1,9 @@
import { queueJob, nextTick, queuePostFlushCb } from '../src/scheduler'
import {
queueJob,
nextTick,
queuePostFlushCb,
invalidateJob
} from '../src/scheduler'
describe('scheduler', () => {
it('nextTick', async () => {
@@ -230,4 +235,31 @@ describe('scheduler', () => {
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
})
})
test('invalidateJob', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
invalidateJob(job2)
job2()
}
const job2 = () => {
calls.push('job2')
}
const job3 = () => {
calls.push('job3')
}
const job4 = () => {
calls.push('job4')
}
// queue all jobs
queueJob(job1)
queueJob(job2)
queueJob(job3)
queuePostFlushCb(job4)
expect(calls).toEqual([])
await nextTick()
// job2 should be called only once
expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
})
})

View File

@@ -1,13 +1,19 @@
import { createVNode } from '@vue/runtime-test'
import {
ShapeFlags,
createBlock,
createVNode,
openBlock,
Comment,
Fragment,
Text,
cloneVNode
} from '@vue/runtime-core'
import { mergeProps, normalizeVNode } from '../src/vnode'
cloneVNode,
mergeProps,
normalizeVNode,
transformVNodeArgs
} from '../src/vnode'
import { Data } from '../src/component'
import { ShapeFlags, PatchFlags } from '@vue/shared'
import { h } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
describe('vnode', () => {
test('create with just tag', () => {
@@ -35,6 +41,23 @@ describe('vnode', () => {
expect(vnode.props).toBe(null)
})
test('vnode keys', () => {
for (const key of ['', 'a', 0, 1, NaN]) {
expect(createVNode('div', { key }).key).toBe(key)
}
expect(createVNode('div').key).toBe(null)
expect(createVNode('div', { key: undefined }).key).toBe(null)
})
test('create with class component', () => {
class Component {
$props: any
static __vccOpts = { template: '<div />' }
}
const vnode = createVNode(Component)
expect(vnode.type).toEqual(Component.__vccOpts)
})
describe('class normalization', () => {
test('string', () => {
const vnode = createVNode('p', { class: 'foo baz' })
@@ -86,7 +109,7 @@ describe('vnode', () => {
const vnode = createVNode('p', null, ['foo'])
expect(vnode.children).toMatchObject(['foo'])
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.ARRAY_CHILDREN
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
)
})
@@ -94,7 +117,7 @@ describe('vnode', () => {
const vnode = createVNode('p', null, { foo: 'foo' })
expect(vnode.children).toMatchObject({ foo: 'foo' })
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
)
})
@@ -102,7 +125,7 @@ describe('vnode', () => {
const vnode = createVNode('p', null, nop)
expect(vnode.children).toMatchObject({ default: nop })
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
)
})
@@ -110,7 +133,19 @@ describe('vnode', () => {
const vnode = createVNode('p', null, 'foo')
expect(vnode.children).toBe('foo')
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.TEXT_CHILDREN
ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
)
})
test('element with slots', () => {
const children = [createVNode('span', null, 'hello')]
const vnode = createVNode('div', null, {
default: () => children
})
expect(vnode.children).toBe(children)
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
)
})
})
@@ -120,6 +155,12 @@ describe('vnode', () => {
expect(normalizeVNode(null)).toMatchObject({ type: Comment })
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })
// boolean -> Comment
// this is for usage like `someBoolean && h('div')` and behavior consistency
// with 2.x (#574)
expect(normalizeVNode(true)).toMatchObject({ type: Comment })
expect(normalizeVNode(false)).toMatchObject({ type: Comment })
// array -> Fragment
expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })
@@ -137,7 +178,6 @@ describe('vnode', () => {
// primitive types
expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })
expect(normalizeVNode(true)).toMatchObject({ type: Text, children: `true` })
})
test('type shapeFlag inference', () => {
@@ -225,4 +265,126 @@ describe('vnode', () => {
})
})
})
describe('dynamic children', () => {
test('with patchFlags', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('should not track vnodes with only HYDRATE_EVENTS flag', () => {
const hoist = createVNode('div')
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
]))
expect(vnode.dynamicChildren).toStrictEqual([])
})
test('many times call openBlock', () => {
const hoist = createVNode('div')
let vnode1, vnode2, vnode3
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
(vnode2 = (openBlock(),
createBlock('div', null, [
hoist,
(vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT))
])))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
})
test('with stateful component', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode({}, null, 'text'))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('with functional component', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode(() => {}, null, 'text'))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('with suspense', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode(() => {}, null, 'text'))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
})
describe('transformVNodeArgs', () => {
afterEach(() => {
// reset
transformVNodeArgs()
})
test('no-op pass through', () => {
transformVNodeArgs(args => args)
const vnode = createVNode('div', { id: 'foo' }, 'hello')
expect(vnode).toMatchObject({
type: 'div',
props: { id: 'foo' },
children: 'hello',
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
})
})
test('direct override', () => {
transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
const vnode = createVNode('p')
expect(vnode).toMatchObject({
type: 'div',
props: { id: 'foo' },
children: 'hello',
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
})
})
test('receive component instance as 2nd arg', () => {
transformVNodeArgs((args, instance) => {
if (instance) {
return ['h1', null, instance.type.name]
} else {
return args
}
})
const App = {
// this will be the name of the component in the h1
name: 'Root Component',
render() {
return h('p') // this will be overwritten by the transform
}
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
})
})
})

View File

@@ -0,0 +1,98 @@
import {
h,
render,
nodeOps,
VNodeProps,
TestElement,
NodeTypes,
VNode
} from '@vue/runtime-test'
describe('renderer: vnode hooks', () => {
function assertHooks(hooks: VNodeProps, vnode1: VNode, vnode2: VNode) {
const root = nodeOps.createElement('div')
render(vnode1, root)
expect(hooks.onVnodeBeforeMount).toHaveBeenCalledWith(vnode1, null)
expect(hooks.onVnodeMounted).toHaveBeenCalledWith(vnode1, null)
expect(hooks.onVnodeBeforeUpdate).not.toHaveBeenCalled()
expect(hooks.onVnodeUpdated).not.toHaveBeenCalled()
expect(hooks.onVnodeBeforeUnmount).not.toHaveBeenCalled()
expect(hooks.onVnodeUnmounted).not.toHaveBeenCalled()
// update
render(vnode2, root)
expect(hooks.onVnodeBeforeMount).toHaveBeenCalledTimes(1)
expect(hooks.onVnodeMounted).toHaveBeenCalledTimes(1)
expect(hooks.onVnodeBeforeUpdate).toHaveBeenCalledWith(vnode2, vnode1)
expect(hooks.onVnodeUpdated).toHaveBeenCalledWith(vnode2, vnode1)
expect(hooks.onVnodeBeforeUnmount).not.toHaveBeenCalled()
expect(hooks.onVnodeUnmounted).not.toHaveBeenCalled()
// unmount
render(null, root)
expect(hooks.onVnodeBeforeMount).toHaveBeenCalledTimes(1)
expect(hooks.onVnodeMounted).toHaveBeenCalledTimes(1)
expect(hooks.onVnodeBeforeUpdate).toHaveBeenCalledTimes(1)
expect(hooks.onVnodeUpdated).toHaveBeenCalledTimes(1)
expect(hooks.onVnodeBeforeUnmount).toHaveBeenCalledWith(vnode2, null)
expect(hooks.onVnodeUnmounted).toHaveBeenCalledWith(vnode2, null)
}
test('should work on element', () => {
const hooks: VNodeProps = {
onVnodeBeforeMount: jest.fn(),
onVnodeMounted: jest.fn(),
onVnodeBeforeUpdate: jest.fn(vnode => {
expect((vnode.el as TestElement).children[0]).toMatchObject({
type: NodeTypes.TEXT,
text: 'foo'
})
}),
onVnodeUpdated: jest.fn(vnode => {
expect((vnode.el as TestElement).children[0]).toMatchObject({
type: NodeTypes.TEXT,
text: 'bar'
})
}),
onVnodeBeforeUnmount: jest.fn(),
onVnodeUnmounted: jest.fn()
}
assertHooks(hooks, h('div', hooks, 'foo'), h('div', hooks, 'bar'))
})
test('should work on component', () => {
const Comp = (props: { msg: string }) => props.msg
const hooks: VNodeProps = {
onVnodeBeforeMount: jest.fn(),
onVnodeMounted: jest.fn(),
onVnodeBeforeUpdate: jest.fn(vnode => {
expect(vnode.el as TestElement).toMatchObject({
type: NodeTypes.TEXT,
text: 'foo'
})
}),
onVnodeUpdated: jest.fn(vnode => {
expect(vnode.el as TestElement).toMatchObject({
type: NodeTypes.TEXT,
text: 'bar'
})
}),
onVnodeBeforeUnmount: jest.fn(),
onVnodeUnmounted: jest.fn()
}
assertHooks(
hooks,
h(Comp, {
...hooks,
msg: 'foo'
}),
h(Comp, {
...hooks,
msg: 'bar'
})
)
})
})