| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463 |
- import {
- Fragment,
- type FunctionalComponent,
- type SetupContext,
- Teleport,
- type TestElement,
- type VNode,
- createApp,
- createBlock,
- createCommentVNode,
- createElementBlock,
- createElementVNode,
- createTextVNode,
- createVNode,
- defineComponent,
- h,
- serializeInner as inner,
- nextTick,
- nodeOps,
- onBeforeMount,
- onBeforeUnmount,
- onUnmounted,
- openBlock,
- ref,
- render,
- renderList,
- renderSlot,
- serialize,
- setBlockTracking,
- withCtx,
- } from '@vue/runtime-test'
- import { PatchFlags, SlotFlags, toDisplayString } 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('<p>foo</p>')
- })
- 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('<div><p>foo</p></div>')
- })
- 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(
- '<p>foo</p>',
- )
- })
- 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(
- '<div><p></p></div>',
- )
- 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(
- '<div><i></i></div>',
- )
- })
- test('PatchFlags: PatchFlags.TEXT', async () => {
- renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
- expect(inner(root)).toBe('<div><p>foo</p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foo</p>',
- )
- renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
- expect(inner(root)).toBe('<div><p>bar</p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p>bar</p>',
- )
- })
- test('PatchFlags: PatchFlags.CLASS', async () => {
- renderWithBlock(() => [
- createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS),
- ])
- expect(inner(root)).toBe('<div><p class="foo"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p class="foo"></p>',
- )
- renderWithBlock(() => [
- createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS),
- ])
- expect(inner(root)).toBe('<div><p class="bar"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p class="bar"></p>',
- )
- })
- test('PatchFlags: PatchFlags.STYLE', async () => {
- renderWithBlock(() => [
- createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE),
- ])
- expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p style="color: red"></p>',
- )
- renderWithBlock(() => [
- createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE),
- ])
- expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p style="color: green"></p>',
- )
- })
- test('PatchFlags: PatchFlags.PROPS', async () => {
- renderWithBlock(() => [
- createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id']),
- ])
- expect(inner(root)).toBe('<div><p id="foo"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p id="foo"></p>',
- )
- renderWithBlock(() => [
- createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id']),
- ])
- expect(inner(root)).toBe('<div><p id="bar"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p id="bar"></p>',
- )
- })
- test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
- let propName = 'foo'
- renderWithBlock(() => [
- createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS),
- ])
- expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p foo="dynamic"></p>',
- )
- propName = 'bar'
- renderWithBlock(() => [
- createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS),
- ])
- expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p bar="dynamic"></p>',
- )
- })
- // 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('<p>foo</p><p>bar</p>')
- expect(block.dynamicChildren!.length).toBe(2)
- expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foo</p>',
- )
- expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
- '<p>bar</p>',
- )
- 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('<p>foofoo</p><p>barbar</p>')
- expect(block.dynamicChildren!.length).toBe(2)
- expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foofoo</p>',
- )
- expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
- '<p>barbar</p>',
- )
- })
- // 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('<p>foo</p>')
- 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('<i>bar</i><p>foo</p>')
- 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('<p>foo</p>')
- 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('<i>bar</i><p>foo</p>')
- expect(block.dynamicChildren!.length).toBe(0)
- })
- test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
- const spyMounted = vi.fn()
- const spyUpdated = vi.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('<div><p></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p></p>',
- )
- expect(spyMounted).toHaveBeenCalledTimes(1)
- expect(spyUpdated).toHaveBeenCalledTimes(0)
- count.value++
- await nextTick()
- expect(inner(root)).toBe('<div><p></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p></p>',
- )
- 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('<div><p>foo</p></div>')
- expect(block!.dynamicChildren!.length).toBe(0)
- render(
- (openBlock(),
- (block = createBlock(
- 'div',
- null,
- [createVNode('i', null, 'bar')],
- PatchFlags.BAIL,
- ))),
- root,
- )
- expect(inner(root)).toBe('<div><i>bar</i></div>')
- 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 createBlock(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('<div><p>0</p></div>')
- 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('<p>0</p>')
- foo.value++
- await nextTick()
- expect(inner(root)).toBe('<div><p>1</p></div>')
- })
- // #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 = vi.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)
- })
- test('should call onUnmounted hook for dynamic components receiving an existing vnode w/ component children', async () => {
- const spy = vi.fn()
- const show = ref(1)
- const Child = {
- setup() {
- onUnmounted(spy)
- return () => 'child'
- },
- }
- const foo = h('div', null, h(Child))
- const app = createApp({
- render() {
- return show.value
- ? (openBlock(),
- createBlock('div', null, [(openBlock(), createBlock(foo))]))
- : createCommentVNode('v-if', true)
- },
- })
- app.mount(root)
- show.value = 0
- await nextTick()
- 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 = vi.fn()
- const spyB = vi.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 = {
- name: 'A',
- setup(props: any, { slots }: SetupContext) {
- return () => {
- return (
- openBlock(),
- createBlock('div', null, [renderSlot(slots, 'default')])
- )
- }
- },
- }
- const Wrapper = {
- name: '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({
- name: 'App',
- 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('<div>Hello</div>')
- state.value = 1
- await nextTick()
- expect(inner(root)).toBe('<div>World</div>')
- })
- //#3623
- test('nested teleport unmount need exit the optimization mode', async () => {
- 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,
- )
- await nextTick()
- expect(inner(target)).toMatchInlineSnapshot(
- `"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
- )
- expect(inner(root)).toMatchInlineSnapshot(
- `"<div><!--teleport start--><!--teleport end--></div>"`,
- )
- 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('<section><div class="foo"></div></section>')
- /**
- * 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('<p>1</p>')
- index.value = 1
- await nextTick()
- expect(inner(root)).toBe('<p>2</p>')
- index.value = 0
- await nextTick()
- expect(inner(root)).toBe('<p>1</p>')
- })
- // #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('<div><div><span>loaded</span></div></div>')
- loading.value = true
- await nextTick()
- expect(inner(root)).toBe('<div><div><span>loading</span></div></div>')
- })
- // #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('<div><div>false</div></div>')
- show.value = true
- await nextTick()
- expect(inner(root)).toBe('<div><div>true</div></div>')
- })
- // #13305
- test('patch Suspense nested in list nodes in optimized mode', async () => {
- const deps: Promise<any>[] = []
- const Item = {
- props: {
- someId: { type: Number, required: true },
- },
- async setup(props: any) {
- const p = new Promise(resolve => setTimeout(resolve, 1))
- deps.push(p)
- await p
- return () => (
- openBlock(),
- createElementBlock('li', null, [
- createElementVNode(
- 'p',
- null,
- String(props.someId),
- PatchFlags.TEXT,
- ),
- ])
- )
- },
- }
- const list = ref([1, 2, 3])
- const App = {
- setup() {
- return () => (
- openBlock(),
- createElementBlock(
- Fragment,
- null,
- [
- createElementVNode(
- 'p',
- null,
- JSON.stringify(list.value),
- PatchFlags.TEXT,
- ),
- createElementVNode('ol', null, [
- (openBlock(),
- createBlock(SuspenseImpl, null, {
- fallback: withCtx(() => [
- createElementVNode('li', null, 'Loading…'),
- ]),
- default: withCtx(() => [
- (openBlock(true),
- createElementBlock(
- Fragment,
- null,
- renderList(list.value, id => {
- return (
- openBlock(),
- createBlock(
- Item,
- {
- key: id,
- 'some-id': id,
- },
- null,
- PatchFlags.PROPS,
- ['some-id'],
- )
- )
- }),
- PatchFlags.KEYED_FRAGMENT,
- )),
- ]),
- _: 1 /* STABLE */,
- })),
- ]),
- ],
- PatchFlags.STABLE_FRAGMENT,
- )
- )
- },
- }
- const app = createApp(App)
- app.mount(root)
- expect(inner(root)).toBe(`<p>[1,2,3]</p>` + `<ol><li>Loading…</li></ol>`)
- await Promise.all(deps)
- await nextTick()
- expect(inner(root)).toBe(
- `<p>[1,2,3]</p>` +
- `<ol>` +
- `<li><p>1</p></li>` +
- `<li><p>2</p></li>` +
- `<li><p>3</p></li>` +
- `</ol>`,
- )
- list.value = [3, 1, 2]
- await nextTick()
- expect(inner(root)).toBe(
- `<p>[3,1,2]</p>` +
- `<ol>` +
- `<li><p>3</p></li>` +
- `<li><p>1</p></li>` +
- `<li><p>2</p></li>` +
- `</ol>`,
- )
- })
- // #4183
- test('should not take unmount children fast path /w Suspense', async () => {
- const show = ref(true)
- const spyUnmounted = vi.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('<div>true</div>')
- show.value = false
- await nextTick()
- expect(inner(root)).toBe('<!--v-if-->')
- 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(`<ul><li>dummy</li></ul>`)
- // unmount
- toggle.value = false
- await nextTick()
- // should successfully unmount without error
- expect(inner(root)).toBe(`<!---->`)
- })
- // #10870
- test('should bail manually rendered compiler slots for both mount and update', async () => {
- // only reproducible in prod
- __DEV__ = false
- function Outer(_: any, { slots }: any) {
- return slots.default()
- }
- const Mid = {
- render(ctx: any) {
- return (
- openBlock(),
- createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
- )
- },
- }
- const state1 = ref(true)
- const state2 = ref(true)
- const App = {
- render() {
- return (
- openBlock(),
- createBlock(Outer, null, {
- default: withCtx(() => [
- createVNode(
- Mid,
- { foo: state2.value },
- {
- default: withCtx(() => [
- createElementVNode('div', null, [
- createElementVNode('div', null, [
- state2.value
- ? (openBlock(),
- createElementBlock(
- 'div',
- {
- key: 0,
- id: 'if',
- foo: state1.value,
- },
- null,
- 8 /* PROPS */,
- ['foo'],
- ))
- : createCommentVNode('v-if', true),
- ]),
- ]),
- ]),
- _: 1 /* STABLE */,
- },
- 8 /* PROPS */,
- ['foo'],
- ),
- ]),
- _: 1 /* STABLE */,
- })
- )
- },
- }
- const app = createApp(App)
- app.config.errorHandler = vi.fn()
- try {
- app.mount(root)
- state1.value = false
- await nextTick()
- state2.value = false
- await nextTick()
- } finally {
- __DEV__ = true
- expect(app.config.errorHandler).not.toHaveBeenCalled()
- }
- })
- // #11336
- test('should bail manually rendered compiler slots for both mount and update (2)', async () => {
- // only reproducible in prod
- __DEV__ = false
- const n = ref(0)
- function Outer(_: any, { slots }: any) {
- n.value // track
- return slots.default()
- }
- const Mid = {
- render(ctx: any) {
- return (
- openBlock(),
- createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
- )
- },
- }
- const show = ref(false)
- const App = {
- render() {
- return (
- openBlock(),
- createBlock(Outer, null, {
- default: withCtx(() => [
- createVNode(Mid, null, {
- default: withCtx(() => [
- createElementVNode('div', null, [
- show.value
- ? (openBlock(),
- createElementBlock('div', { key: 0 }, '1'))
- : createCommentVNode('v-if', true),
- createElementVNode('div', null, '2'),
- createElementVNode('div', null, '3'),
- ]),
- createElementVNode('div', null, '4'),
- ]),
- _: 1 /* STABLE */,
- }),
- ]),
- _: 1 /* STABLE */,
- })
- )
- },
- }
- const app = createApp(App)
- app.config.errorHandler = vi.fn()
- try {
- app.mount(root)
- // force Outer update, which will assign new slots to Mid
- // we want to make sure the compiled slot flag doesn't accidentally
- // get assigned again
- n.value++
- await nextTick()
- show.value = true
- await nextTick()
- } finally {
- __DEV__ = true
- expect(app.config.errorHandler).not.toHaveBeenCalled()
- }
- })
- test('diff slot and slot fallback node', async () => {
- const Comp = {
- props: ['show'],
- setup(props: any, { slots }: SetupContext) {
- return () => {
- return (
- openBlock(),
- createElementBlock('div', null, [
- renderSlot(slots, 'default', { hide: !props.show }, () => [
- (openBlock(),
- (block = createElementBlock(
- Fragment,
- { key: 0 },
- [createTextVNode('foo')],
- PatchFlags.STABLE_FRAGMENT,
- ))),
- ]),
- ])
- )
- }
- },
- }
- const show = ref(true)
- const app = createApp({
- render() {
- return (
- openBlock(),
- createBlock(
- Comp,
- { show: show.value },
- {
- default: withCtx(({ hide }: { hide: boolean }) => [
- !hide
- ? (openBlock(),
- createElementBlock(
- Fragment,
- { key: 0 },
- [
- createCommentVNode('comment'),
- createElementVNode(
- 'div',
- null,
- 'bar',
- PatchFlags.CACHED,
- ),
- ],
- PatchFlags.STABLE_FRAGMENT,
- ))
- : createCommentVNode('v-if', true),
- ]),
- _: SlotFlags.STABLE,
- },
- PatchFlags.PROPS,
- ['show'],
- )
- )
- },
- })
- app.mount(root)
- expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
- expect(block).toBe(null)
- show.value = false
- await nextTick()
- expect(inner(root)).toBe('<div>foo</div>')
- show.value = true
- await nextTick()
- expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
- })
- test('should not take unmount children fast path if children contain cached nodes', async () => {
- const show = ref(true)
- const spyUnmounted = vi.fn()
- const Child = {
- setup() {
- onUnmounted(spyUnmounted)
- return () => createVNode('div', null, 'Child')
- },
- }
- const app = createApp({
- render(_: any, cache: any) {
- return show.value
- ? (openBlock(),
- createBlock('div', null, [
- createVNode('div', null, [
- cache[0] ||
- (setBlockTracking(-1, true),
- ((cache[0] = createVNode('div', null, [
- createVNode(Child),
- ])).cacheIndex = 0),
- setBlockTracking(1),
- cache[0]),
- ]),
- ]))
- : createCommentVNode('v-if', true)
- },
- })
- app.mount(root)
- expect(inner(root)).toBe(
- '<div><div><div><div>Child</div></div></div></div>',
- )
- show.value = false
- await nextTick()
- expect(inner(root)).toBe('<!--v-if-->')
- expect(spyUnmounted).toHaveBeenCalledTimes(1)
- show.value = true
- await nextTick()
- expect(inner(root)).toBe(
- '<div><div><div><div>Child</div></div></div></div>',
- )
- // should unmount again, this verifies previous cache was properly cleared
- show.value = false
- await nextTick()
- expect(inner(root)).toBe('<!--v-if-->')
- expect(spyUnmounted).toHaveBeenCalledTimes(2)
- })
- // #12371
- test('unmount children when the user calls a compiled slot', async () => {
- const beforeMountSpy = vi.fn()
- const beforeUnmountSpy = vi.fn()
- const Child = {
- setup() {
- onBeforeMount(beforeMountSpy)
- onBeforeUnmount(beforeUnmountSpy)
- return () => 'child'
- },
- }
- const Wrapper = {
- setup(_: any, { slots }: SetupContext) {
- return () => (
- openBlock(),
- createElementBlock('section', null, [
- (openBlock(),
- createElementBlock('div', { key: 1 }, [
- createTextVNode(slots.header!() ? 'foo' : 'bar', 1 /* TEXT */),
- renderSlot(slots, 'content'),
- ])),
- ])
- )
- },
- }
- const show = ref(false)
- const app = createApp({
- render() {
- return show.value
- ? (openBlock(),
- createBlock(Wrapper, null, {
- header: withCtx(() => [createVNode({})]),
- content: withCtx(() => [createVNode(Child)]),
- _: 1,
- }))
- : createCommentVNode('v-if', true)
- },
- })
- app.mount(root)
- expect(inner(root)).toMatchInlineSnapshot(`"<!--v-if-->"`)
- expect(beforeMountSpy).toHaveBeenCalledTimes(0)
- expect(beforeUnmountSpy).toHaveBeenCalledTimes(0)
- show.value = true
- await nextTick()
- expect(inner(root)).toMatchInlineSnapshot(
- `"<section><div>foochild</div></section>"`,
- )
- expect(beforeMountSpy).toHaveBeenCalledTimes(1)
- show.value = false
- await nextTick()
- expect(inner(root)).toBe('<!--v-if-->')
- expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
- })
- // #12411
- test('handle patch stable fragment with non-reactive v-for source', async () => {
- const count = ref(0)
- const foo: any = []
- function updateFoo() {
- for (let n = 0; n < 3; n++) {
- foo[n] = n + 1 + '_foo'
- }
- }
- const Comp = {
- setup() {
- return () => {
- // <div>{{ count }}</div>
- // <div v-for='item in foo'>{{ item }}</div>
- return (
- openBlock(),
- createElementBlock(
- Fragment,
- null,
- [
- createElementVNode(
- 'div',
- null,
- toDisplayString(count.value),
- PatchFlags.TEXT,
- ),
- (openBlock(),
- createElementBlock(
- Fragment,
- null,
- renderList(foo, item => {
- return createElementVNode(
- 'div',
- null,
- toDisplayString(item),
- PatchFlags.TEXT,
- )
- }),
- PatchFlags.STABLE_FRAGMENT,
- )),
- ],
- PatchFlags.STABLE_FRAGMENT,
- )
- )
- }
- },
- }
- render(h(Comp), root)
- expect(inner(root)).toBe('<div>0</div>')
- updateFoo()
- count.value++
- await nextTick()
- expect(inner(root)).toBe(
- '<div>1</div><div>1_foo</div><div>2_foo</div><div>3_foo</div>',
- )
- })
- })
|