import { h, Fragment, createVNode, openBlock, createBlock, render, nodeOps, TestElement, serialize, serializeInner as inner, VNode, ref, nextTick, defineComponent, withCtx, renderSlot, onBeforeUnmount } from '@vue/runtime-test' import { PatchFlags, SlotFlags } from '@vue/shared' 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('

foo

') }) 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('

foo

') 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('

bar

') 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('bar

foo

') 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('bar

foo

') 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('

foo

') 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('

0

') 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('

1

') }) // #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) }) })