import {
h,
Fragment,
Teleport,
createVNode,
createCommentVNode,
openBlock,
createBlock,
render,
nodeOps,
TestElement,
serialize,
serializeInner as inner,
VNode,
ref,
nextTick,
defineComponent,
withCtx,
renderSlot,
onBeforeUnmount,
createTextVNode,
SetupContext,
createApp,
FunctionalComponent,
renderList,
onUnmounted
} from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { SuspenseImpl } from '../src/components/Suspense'
describe('renderer: optimized mode', () => {
let root: TestElement
let block: VNode | null = null
beforeEach(() => {
root = nodeOps.createElement('div')
block = null
})
const renderWithBlock = (renderChildren: () => VNode[]) => {
render(
(openBlock(), (block = createBlock('div', null, renderChildren()))),
root
)
}
test('basic use of block', () => {
render((openBlock(), (block = createBlock('p', null, 'foo'))), root)
expect(block.dynamicChildren!.length).toBe(0)
expect(inner(root)).toBe('
foo
')
})
test('block can appear anywhere in the vdom tree', () => {
render(
h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
root
)
expect(block.dynamicChildren!.length).toBe(0)
expect(inner(root)).toBe('')
})
test('block should collect dynamic vnodes', () => {
renderWithBlock(() => [
createVNode('p', null, 'foo', PatchFlags.TEXT),
createVNode('i')
])
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
'foo
'
)
})
test('block can disable tracking', () => {
render(
// disable tracking
(openBlock(true),
(block = createBlock('div', null, [
createVNode('p', null, 'foo', PatchFlags.TEXT)
]))),
root
)
expect(block.dynamicChildren!.length).toBe(0)
})
test('block as dynamic children', () => {
renderWithBlock(() => [
(openBlock(), createBlock('div', { key: 0 }, [h('p')]))
])
expect(block!.dynamicChildren!.length).toBe(1)
expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
renderWithBlock(() => [
(openBlock(), createBlock('div', { key: 1 }, [h('i')]))
])
expect(block!.dynamicChildren!.length).toBe(1)
expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
'
'
)
})
test('PatchFlags: PatchFlags.TEXT', async () => {
renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
'foo
'
)
renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
'bar
'
)
})
test('PatchFlags: PatchFlags.CLASS', async () => {
renderWithBlock(() => [
createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS)
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
renderWithBlock(() => [
createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS)
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
})
test('PatchFlags: PatchFlags.STYLE', async () => {
renderWithBlock(() => [
createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE)
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
renderWithBlock(() => [
createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE)
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
})
test('PatchFlags: PatchFlags.PROPS', async () => {
renderWithBlock(() => [
createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id'])
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
renderWithBlock(() => [
createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id'])
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
})
test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
let propName = 'foo'
renderWithBlock(() => [
createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
propName = 'bar'
renderWithBlock(() => [
createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
])
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
})
// the order and length of the list will not change
test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
let list = ['foo', 'bar']
render(
(openBlock(),
(block = createBlock(
Fragment,
null,
list.map(item => {
return createVNode('p', null, item, PatchFlags.TEXT)
}),
PatchFlags.STABLE_FRAGMENT
))),
root
)
expect(inner(root)).toBe('foo
bar
')
expect(block.dynamicChildren!.length).toBe(2)
expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
'foo
'
)
expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
'bar
'
)
list = list.map(item => item.repeat(2))
render(
(openBlock(),
createBlock(
Fragment,
null,
list.map(item => {
return createVNode('p', null, item, PatchFlags.TEXT)
}),
PatchFlags.STABLE_FRAGMENT
)),
root
)
expect(inner(root)).toBe('foofoo
barbar
')
expect(block.dynamicChildren!.length).toBe(2)
expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
'foofoo
'
)
expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
'barbar
'
)
})
// A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
// so there's no need for tracking dynamicChildren.
test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
const list = [{ tag: 'p', text: 'foo' }]
render(
(openBlock(true),
(block = createBlock(
Fragment,
null,
list.map(item => {
return createVNode(item.tag, null, item.text)
}),
PatchFlags.UNKEYED_FRAGMENT
))),
root
)
expect(inner(root)).toBe('foo
')
expect(block.dynamicChildren!.length).toBe(0)
list.unshift({ tag: 'i', text: 'bar' })
render(
(openBlock(true),
createBlock(
Fragment,
null,
list.map(item => {
return createVNode(item.tag, null, item.text)
}),
PatchFlags.UNKEYED_FRAGMENT
)),
root
)
expect(inner(root)).toBe('barfoo
')
expect(block.dynamicChildren!.length).toBe(0)
})
// A Fragment with `KEYED_FRAGMENT` will always patch its children,
// so there's no need for tracking dynamicChildren.
test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
const list = [{ tag: 'p', text: 'foo' }]
render(
(openBlock(true),
(block = createBlock(
Fragment,
null,
list.map(item => {
return createVNode(item.tag, { key: item.tag }, item.text)
}),
PatchFlags.KEYED_FRAGMENT
))),
root
)
expect(inner(root)).toBe('foo
')
expect(block.dynamicChildren!.length).toBe(0)
list.unshift({ tag: 'i', text: 'bar' })
render(
(openBlock(true),
createBlock(
Fragment,
null,
list.map(item => {
return createVNode(item.tag, { key: item.tag }, item.text)
}),
PatchFlags.KEYED_FRAGMENT
)),
root
)
expect(inner(root)).toBe('barfoo
')
expect(block.dynamicChildren!.length).toBe(0)
})
test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
const spyMounted = jest.fn()
const spyUpdated = jest.fn()
const count = ref(0)
const Comp = {
setup() {
return () => {
count.value
return (
openBlock(),
(block = createBlock('div', null, [
createVNode(
'p',
{ onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
'',
PatchFlags.NEED_PATCH
)
]))
)
}
}
}
render(h(Comp), root)
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
expect(spyMounted).toHaveBeenCalledTimes(1)
expect(spyUpdated).toHaveBeenCalledTimes(0)
count.value++
await nextTick()
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
''
)
expect(spyMounted).toHaveBeenCalledTimes(1)
expect(spyUpdated).toHaveBeenCalledTimes(1)
})
test('PatchFlags: PatchFlags.BAIL', async () => {
render(
(openBlock(),
(block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
root
)
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(0)
render(
(openBlock(),
(block = createBlock(
'div',
null,
[createVNode('i', null, 'bar')],
PatchFlags.BAIL
))),
root
)
expect(inner(root)).toBe('bar
')
expect(block!.dynamicChildren).toBe(null)
})
// #1980
test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
let block: VNode
const Comp = defineComponent({
setup(_props, { slots }) {
return () => {
const vnode =
(openBlock(),
(block = createBlock('div', null, {
default: withCtx(() => [renderSlot(slots, 'default')]),
_: SlotFlags.FORWARDED
})))
return vnode
}
}
})
const foo = ref(0)
const App = {
setup() {
return () => {
return createVNode(Comp, null, {
default: withCtx(() => [
createVNode('p', null, foo.value, PatchFlags.TEXT)
]),
// Indicates that this is a stable slot to avoid bail out
_: SlotFlags.STABLE
})
}
}
}
render(h(App), root)
expect(inner(root)).toBe('')
expect(block!.dynamicChildren!.length).toBe(1)
expect(block!.dynamicChildren![0].type).toBe(Fragment)
expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
expect(
serialize(
block!.dynamicChildren![0].dynamicChildren![0].el as TestElement
)
).toBe('0
')
foo.value++
await nextTick()
expect(inner(root)).toBe('')
})
// #2169
// block
// - dynamic child (1)
// - component (2)
// When unmounting (1), we know we are in optimized mode so no need to further
// traverse unmount its children
test('should not perform unnecessary unmount traversals', () => {
const spy = jest.fn()
const Child = {
setup() {
onBeforeUnmount(spy)
return () => 'child'
}
}
const Parent = () => (
openBlock(),
createBlock('div', null, [
createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */)
])
)
render(h(Parent), root)
render(null, root)
expect(spy).toHaveBeenCalledTimes(1)
})
// #2444
// `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
test('non-stable Fragment always need to diff its children', () => {
const spyA = jest.fn()
const spyB = jest.fn()
const ChildA = {
setup() {
onBeforeUnmount(spyA)
return () => 'child'
}
}
const ChildB = {
setup() {
onBeforeUnmount(spyB)
return () => 'child'
}
}
const Parent = () => (
openBlock(),
createBlock('div', null, [
(openBlock(true),
createBlock(
Fragment,
null,
[createVNode(ChildA, { key: 0 })],
128 /* KEYED_FRAGMENT */
)),
(openBlock(true),
createBlock(
Fragment,
null,
[createVNode(ChildB)],
256 /* UNKEYED_FRAGMENT */
))
])
)
render(h(Parent), root)
render(null, root)
expect(spyA).toHaveBeenCalledTimes(1)
expect(spyB).toHaveBeenCalledTimes(1)
})
// #2893
test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
const state = ref(0)
const CompA = {
setup(props: any, { slots }: SetupContext) {
return () => {
return (
openBlock(),
createBlock('div', null, [renderSlot(slots, 'default')])
)
}
}
}
const Wrapper = {
setup(props: any, { slots }: SetupContext) {
// use the manually written render function to rendering the optimized slots,
// which should make subsequent updates exit the optimized mode correctly
return () => {
return slots.default!()[state.value]
}
}
}
const app = createApp({
setup() {
return () => {
return (
openBlock(),
createBlock(Wrapper, null, {
default: withCtx(() => [
createVNode(CompA, null, {
default: withCtx(() => [createTextVNode('Hello')]),
_: 1 /* STABLE */
}),
createVNode(CompA, null, {
default: withCtx(() => [createTextVNode('World')]),
_: 1 /* STABLE */
})
]),
_: 1 /* STABLE */
})
)
}
}
})
app.mount(root)
expect(inner(root)).toBe('Hello
')
state.value = 1
await nextTick()
expect(inner(root)).toBe('World
')
})
//#3623
test('nested teleport unmount need exit the optimization mode', () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
render(
(openBlock(),
createBlock('div', null, [
(openBlock(),
createBlock(
Teleport as any,
{
to: target
},
[
createVNode('div', null, [
(openBlock(),
createBlock(
Teleport as any,
{
to: target
},
[createVNode('div', null, 'foo')]
))
])
]
))
])),
root
)
expect(inner(target)).toMatchInlineSnapshot(
`"foo
"`
)
expect(inner(root)).toMatchInlineSnapshot(
`""`
)
render(null, root)
expect(inner(target)).toBe('')
})
// #3548
test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
const Comp = {
setup(props: any, { slots }: SetupContext) {
return () => {
return (
openBlock(),
(block = createBlock('section', null, [
renderSlot(slots, 'default')
]))
)
}
}
}
let dynamicVNode: VNode
const Wrapper = {
setup(props: any, { slots }: SetupContext) {
return () => {
return (
openBlock(),
createBlock(Comp, null, {
default: withCtx(() => {
return [
(dynamicVNode = createVNode(
'div',
{
class: {
foo: !!slots.default!()
}
},
null,
PatchFlags.CLASS
))
]
}),
_: 1
})
)
}
}
}
const app = createApp({
render() {
return (
openBlock(),
createBlock(Wrapper, null, {
default: withCtx(() => {
return [createVNode({}) /* component */]
}),
_: 1
})
)
}
})
app.mount(root)
expect(inner(root)).toBe('')
/**
* Block Tree:
* - block(div)
* - block(Fragment): renderSlots()
* - dynamicVNode
*/
expect(block!.dynamicChildren!.length).toBe(1)
expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
dynamicVNode!
)
})
// 3569
test('should force bailout when the user manually calls the slot function', async () => {
const index = ref(0)
const Foo = {
setup(props: any, { slots }: SetupContext) {
return () => {
return slots.default!()[index.value]
}
}
}
const app = createApp({
setup() {
return () => {
return (
openBlock(),
createBlock(Foo, null, {
default: withCtx(() => [
true
? (openBlock(), createBlock('p', { key: 0 }, '1'))
: createCommentVNode('v-if', true),
true
? (openBlock(), createBlock('p', { key: 0 }, '2'))
: createCommentVNode('v-if', true)
]),
_: 1 /* STABLE */
})
)
}
}
})
app.mount(root)
expect(inner(root)).toBe('1
')
index.value = 1
await nextTick()
expect(inner(root)).toBe('2
')
index.value = 0
await nextTick()
expect(inner(root)).toBe('1
')
})
// #3779
test('treat slots manually written by the user as dynamic', async () => {
const Middle = {
setup(props: any, { slots }: any) {
return slots.default!
}
}
const Comp = {
setup(props: any, { slots }: any) {
return () => {
return (
openBlock(),
createBlock('div', null, [
createVNode(Middle, null, {
default: withCtx(
() => [
createVNode('div', null, [renderSlot(slots, 'default')])
],
undefined
),
_: 3 /* FORWARDED */
})
])
)
}
}
}
const loading = ref(false)
const app = createApp({
setup() {
return () => {
// important: write the slot content here
const content = h('span', loading.value ? 'loading' : 'loaded')
return h(Comp, null, {
default: () => content
})
}
}
})
app.mount(root)
expect(inner(root)).toBe('')
loading.value = true
await nextTick()
expect(inner(root)).toBe('')
})
// #3828
test('patch Suspense in optimized mode w/ nested dynamic nodes', async () => {
const show = ref(false)
const app = createApp({
render() {
return (
openBlock(),
createBlock(
Fragment,
null,
[
(openBlock(),
createBlock(SuspenseImpl, null, {
default: withCtx(() => [
createVNode('div', null, [
createVNode('div', null, show.value, PatchFlags.TEXT)
])
]),
_: SlotFlags.STABLE
}))
],
PatchFlags.STABLE_FRAGMENT
)
)
}
})
app.mount(root)
expect(inner(root)).toBe('')
show.value = true
await nextTick()
expect(inner(root)).toBe('')
})
// #4183
test('should not take unmount children fast path /w Suspense', async () => {
const show = ref(true)
const spyUnmounted = jest.fn()
const Parent = {
setup(props: any, { slots }: SetupContext) {
return () => (
openBlock(),
createBlock(SuspenseImpl, null, {
default: withCtx(() => [renderSlot(slots, 'default')]),
_: SlotFlags.FORWARDED
})
)
}
}
const Child = {
setup() {
onUnmounted(spyUnmounted)
return () => createVNode('div', null, show.value, PatchFlags.TEXT)
}
}
const app = createApp({
render() {
return show.value
? (openBlock(),
createBlock(
Parent,
{ key: 0 },
{
default: withCtx(() => [createVNode(Child)]),
_: SlotFlags.STABLE
}
))
: createCommentVNode('v-if', true)
}
})
app.mount(root)
expect(inner(root)).toBe('true
')
show.value = false
await nextTick()
expect(inner(root)).toBe('')
expect(spyUnmounted).toHaveBeenCalledTimes(1)
})
// #3881
// root cause: fragment inside a compiled slot passed to component which
// programmatically invokes the slot. The entire slot should de-opt but
// the fragment was incorrectly put in optimized mode which causes it to skip
// updates for its inner components.
test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
const Parent: FunctionalComponent = (_, { slots }) => slots.default!()
const Dummy = () => 'dummy'
const toggle = ref(true)
const force = ref(0)
const app = createApp({
render() {
if (!toggle.value) {
return null
}
return h(
Parent,
{ n: force.value },
{
default: withCtx(
() => [
createVNode('ul', null, [
(openBlock(),
createBlock(
Fragment,
null,
renderList(1, item => {
return createVNode('li', null, [createVNode(Dummy)])
}),
64 /* STABLE_FRAGMENT */
))
])
],
undefined,
true
),
_: 1 /* STABLE */
}
)
}
})
app.mount(root)
// force a patch
force.value++
await nextTick()
expect(inner(root)).toBe(``)
// unmount
toggle.value = false
await nextTick()
// should successfully unmount without error
expect(inner(root)).toBe(``)
})
})