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:
@@ -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>"`;
|
||||
610
packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
Normal file
610
packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
Normal 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('<!---->')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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() {}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
106
packages/runtime-core/__tests__/componentEmits.spec.ts
Normal file
106
packages/runtime-core/__tests__/componentEmits.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
232
packages/runtime-core/__tests__/componentProps.spec.ts
Normal file
232
packages/runtime-core/__tests__/componentProps.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
302
packages/runtime-core/__tests__/components/Teleport.spec.ts
Normal file
302
packages/runtime-core/__tests__/components/Teleport.spec.ts
Normal 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('')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -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>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
68
packages/runtime-core/__tests__/helpers/scopeId.spec.ts
Normal file
68
packages/runtime-core/__tests__/helpers/scopeId.spec.ts
Normal 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>`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toHandlers } from '../../src/helpers/toHandlers'
|
||||
import { mockWarn } from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('toHandlers', () => {
|
||||
mockWarn()
|
||||
|
||||
148
packages/runtime-core/__tests__/hmr.spec.ts
Normal file
148
packages/runtime-core/__tests__/hmr.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
660
packages/runtime-core/__tests__/hydration.spec.ts
Normal file
660
packages/runtime-core/__tests__/hydration.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
NodeTypes,
|
||||
TestElement,
|
||||
serialize,
|
||||
serializeInner,
|
||||
mockWarn
|
||||
serializeInner
|
||||
} from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
mockWarn()
|
||||
|
||||
|
||||
@@ -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>')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
98
packages/runtime-core/__tests__/vnodeHooks.spec.ts
Normal file
98
packages/runtime-core/__tests__/vnodeHooks.spec.ts
Normal 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'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user