rendererOptimizedMode.spec.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. import {
  2. Fragment,
  3. type FunctionalComponent,
  4. type SetupContext,
  5. Teleport,
  6. type TestElement,
  7. type VNode,
  8. createApp,
  9. createBlock,
  10. createCommentVNode,
  11. createTextVNode,
  12. createVNode,
  13. defineComponent,
  14. h,
  15. serializeInner as inner,
  16. nextTick,
  17. nodeOps,
  18. onBeforeUnmount,
  19. onUnmounted,
  20. openBlock,
  21. ref,
  22. render,
  23. renderList,
  24. renderSlot,
  25. serialize,
  26. withCtx,
  27. } from '@vue/runtime-test'
  28. import { PatchFlags, SlotFlags } from '@vue/shared'
  29. import { SuspenseImpl } from '../src/components/Suspense'
  30. describe('renderer: optimized mode', () => {
  31. let root: TestElement
  32. let block: VNode | null = null
  33. beforeEach(() => {
  34. root = nodeOps.createElement('div')
  35. block = null
  36. })
  37. const renderWithBlock = (renderChildren: () => VNode[]) => {
  38. render(
  39. (openBlock(), (block = createBlock('div', null, renderChildren()))),
  40. root,
  41. )
  42. }
  43. test('basic use of block', () => {
  44. render((openBlock(), (block = createBlock('p', null, 'foo'))), root)
  45. expect(block.dynamicChildren!.length).toBe(0)
  46. expect(inner(root)).toBe('<p>foo</p>')
  47. })
  48. test('block can appear anywhere in the vdom tree', () => {
  49. render(
  50. h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
  51. root,
  52. )
  53. expect(block.dynamicChildren!.length).toBe(0)
  54. expect(inner(root)).toBe('<div><p>foo</p></div>')
  55. })
  56. test('block should collect dynamic vnodes', () => {
  57. renderWithBlock(() => [
  58. createVNode('p', null, 'foo', PatchFlags.TEXT),
  59. createVNode('i'),
  60. ])
  61. expect(block!.dynamicChildren!.length).toBe(1)
  62. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  63. '<p>foo</p>',
  64. )
  65. })
  66. test('block can disable tracking', () => {
  67. render(
  68. // disable tracking
  69. (openBlock(true),
  70. (block = createBlock('div', null, [
  71. createVNode('p', null, 'foo', PatchFlags.TEXT),
  72. ]))),
  73. root,
  74. )
  75. expect(block.dynamicChildren!.length).toBe(0)
  76. })
  77. test('block as dynamic children', () => {
  78. renderWithBlock(() => [
  79. (openBlock(), createBlock('div', { key: 0 }, [h('p')])),
  80. ])
  81. expect(block!.dynamicChildren!.length).toBe(1)
  82. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  83. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  84. '<div><p></p></div>',
  85. )
  86. renderWithBlock(() => [
  87. (openBlock(), createBlock('div', { key: 1 }, [h('i')])),
  88. ])
  89. expect(block!.dynamicChildren!.length).toBe(1)
  90. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  91. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  92. '<div><i></i></div>',
  93. )
  94. })
  95. test('PatchFlags: PatchFlags.TEXT', async () => {
  96. renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
  97. expect(inner(root)).toBe('<div><p>foo</p></div>')
  98. expect(block!.dynamicChildren!.length).toBe(1)
  99. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  100. '<p>foo</p>',
  101. )
  102. renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
  103. expect(inner(root)).toBe('<div><p>bar</p></div>')
  104. expect(block!.dynamicChildren!.length).toBe(1)
  105. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  106. '<p>bar</p>',
  107. )
  108. })
  109. test('PatchFlags: PatchFlags.CLASS', async () => {
  110. renderWithBlock(() => [
  111. createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS),
  112. ])
  113. expect(inner(root)).toBe('<div><p class="foo"></p></div>')
  114. expect(block!.dynamicChildren!.length).toBe(1)
  115. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  116. '<p class="foo"></p>',
  117. )
  118. renderWithBlock(() => [
  119. createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS),
  120. ])
  121. expect(inner(root)).toBe('<div><p class="bar"></p></div>')
  122. expect(block!.dynamicChildren!.length).toBe(1)
  123. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  124. '<p class="bar"></p>',
  125. )
  126. })
  127. test('PatchFlags: PatchFlags.STYLE', async () => {
  128. renderWithBlock(() => [
  129. createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE),
  130. ])
  131. expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
  132. expect(block!.dynamicChildren!.length).toBe(1)
  133. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  134. '<p style="color: red"></p>',
  135. )
  136. renderWithBlock(() => [
  137. createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE),
  138. ])
  139. expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
  140. expect(block!.dynamicChildren!.length).toBe(1)
  141. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  142. '<p style="color: green"></p>',
  143. )
  144. })
  145. test('PatchFlags: PatchFlags.PROPS', async () => {
  146. renderWithBlock(() => [
  147. createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id']),
  148. ])
  149. expect(inner(root)).toBe('<div><p id="foo"></p></div>')
  150. expect(block!.dynamicChildren!.length).toBe(1)
  151. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  152. '<p id="foo"></p>',
  153. )
  154. renderWithBlock(() => [
  155. createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id']),
  156. ])
  157. expect(inner(root)).toBe('<div><p id="bar"></p></div>')
  158. expect(block!.dynamicChildren!.length).toBe(1)
  159. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  160. '<p id="bar"></p>',
  161. )
  162. })
  163. test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
  164. let propName = 'foo'
  165. renderWithBlock(() => [
  166. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS),
  167. ])
  168. expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
  169. expect(block!.dynamicChildren!.length).toBe(1)
  170. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  171. '<p foo="dynamic"></p>',
  172. )
  173. propName = 'bar'
  174. renderWithBlock(() => [
  175. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS),
  176. ])
  177. expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
  178. expect(block!.dynamicChildren!.length).toBe(1)
  179. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  180. '<p bar="dynamic"></p>',
  181. )
  182. })
  183. // the order and length of the list will not change
  184. test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
  185. let list = ['foo', 'bar']
  186. render(
  187. (openBlock(),
  188. (block = createBlock(
  189. Fragment,
  190. null,
  191. list.map(item => {
  192. return createVNode('p', null, item, PatchFlags.TEXT)
  193. }),
  194. PatchFlags.STABLE_FRAGMENT,
  195. ))),
  196. root,
  197. )
  198. expect(inner(root)).toBe('<p>foo</p><p>bar</p>')
  199. expect(block.dynamicChildren!.length).toBe(2)
  200. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  201. '<p>foo</p>',
  202. )
  203. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  204. '<p>bar</p>',
  205. )
  206. list = list.map(item => item.repeat(2))
  207. render(
  208. (openBlock(),
  209. createBlock(
  210. Fragment,
  211. null,
  212. list.map(item => {
  213. return createVNode('p', null, item, PatchFlags.TEXT)
  214. }),
  215. PatchFlags.STABLE_FRAGMENT,
  216. )),
  217. root,
  218. )
  219. expect(inner(root)).toBe('<p>foofoo</p><p>barbar</p>')
  220. expect(block.dynamicChildren!.length).toBe(2)
  221. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  222. '<p>foofoo</p>',
  223. )
  224. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  225. '<p>barbar</p>',
  226. )
  227. })
  228. // A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
  229. // so there's no need for tracking dynamicChildren.
  230. test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
  231. const list = [{ tag: 'p', text: 'foo' }]
  232. render(
  233. (openBlock(true),
  234. (block = createBlock(
  235. Fragment,
  236. null,
  237. list.map(item => {
  238. return createVNode(item.tag, null, item.text)
  239. }),
  240. PatchFlags.UNKEYED_FRAGMENT,
  241. ))),
  242. root,
  243. )
  244. expect(inner(root)).toBe('<p>foo</p>')
  245. expect(block.dynamicChildren!.length).toBe(0)
  246. list.unshift({ tag: 'i', text: 'bar' })
  247. render(
  248. (openBlock(true),
  249. createBlock(
  250. Fragment,
  251. null,
  252. list.map(item => {
  253. return createVNode(item.tag, null, item.text)
  254. }),
  255. PatchFlags.UNKEYED_FRAGMENT,
  256. )),
  257. root,
  258. )
  259. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  260. expect(block.dynamicChildren!.length).toBe(0)
  261. })
  262. // A Fragment with `KEYED_FRAGMENT` will always patch its children,
  263. // so there's no need for tracking dynamicChildren.
  264. test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
  265. const list = [{ tag: 'p', text: 'foo' }]
  266. render(
  267. (openBlock(true),
  268. (block = createBlock(
  269. Fragment,
  270. null,
  271. list.map(item => {
  272. return createVNode(item.tag, { key: item.tag }, item.text)
  273. }),
  274. PatchFlags.KEYED_FRAGMENT,
  275. ))),
  276. root,
  277. )
  278. expect(inner(root)).toBe('<p>foo</p>')
  279. expect(block.dynamicChildren!.length).toBe(0)
  280. list.unshift({ tag: 'i', text: 'bar' })
  281. render(
  282. (openBlock(true),
  283. createBlock(
  284. Fragment,
  285. null,
  286. list.map(item => {
  287. return createVNode(item.tag, { key: item.tag }, item.text)
  288. }),
  289. PatchFlags.KEYED_FRAGMENT,
  290. )),
  291. root,
  292. )
  293. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  294. expect(block.dynamicChildren!.length).toBe(0)
  295. })
  296. test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
  297. const spyMounted = vi.fn()
  298. const spyUpdated = vi.fn()
  299. const count = ref(0)
  300. const Comp = {
  301. setup() {
  302. return () => {
  303. count.value
  304. return (
  305. openBlock(),
  306. (block = createBlock('div', null, [
  307. createVNode(
  308. 'p',
  309. { onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
  310. '',
  311. PatchFlags.NEED_PATCH,
  312. ),
  313. ]))
  314. )
  315. }
  316. },
  317. }
  318. render(h(Comp), root)
  319. expect(inner(root)).toBe('<div><p></p></div>')
  320. expect(block!.dynamicChildren!.length).toBe(1)
  321. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  322. '<p></p>',
  323. )
  324. expect(spyMounted).toHaveBeenCalledTimes(1)
  325. expect(spyUpdated).toHaveBeenCalledTimes(0)
  326. count.value++
  327. await nextTick()
  328. expect(inner(root)).toBe('<div><p></p></div>')
  329. expect(block!.dynamicChildren!.length).toBe(1)
  330. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  331. '<p></p>',
  332. )
  333. expect(spyMounted).toHaveBeenCalledTimes(1)
  334. expect(spyUpdated).toHaveBeenCalledTimes(1)
  335. })
  336. test('PatchFlags: PatchFlags.BAIL', async () => {
  337. render(
  338. (openBlock(),
  339. (block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
  340. root,
  341. )
  342. expect(inner(root)).toBe('<div><p>foo</p></div>')
  343. expect(block!.dynamicChildren!.length).toBe(0)
  344. render(
  345. (openBlock(),
  346. (block = createBlock(
  347. 'div',
  348. null,
  349. [createVNode('i', null, 'bar')],
  350. PatchFlags.BAIL,
  351. ))),
  352. root,
  353. )
  354. expect(inner(root)).toBe('<div><i>bar</i></div>')
  355. expect(block!.dynamicChildren).toBe(null)
  356. })
  357. // #1980
  358. test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
  359. let block: VNode
  360. const Comp = defineComponent({
  361. setup(_props, { slots }) {
  362. return () => {
  363. const vnode =
  364. (openBlock(),
  365. (block = createBlock('div', null, {
  366. default: withCtx(() => [renderSlot(slots, 'default')]),
  367. _: SlotFlags.FORWARDED,
  368. })))
  369. return vnode
  370. }
  371. },
  372. })
  373. const foo = ref(0)
  374. const App = {
  375. setup() {
  376. return () => {
  377. return createVNode(Comp, null, {
  378. default: withCtx(() => [
  379. createVNode('p', null, foo.value, PatchFlags.TEXT),
  380. ]),
  381. // Indicates that this is a stable slot to avoid bail out
  382. _: SlotFlags.STABLE,
  383. })
  384. }
  385. },
  386. }
  387. render(h(App), root)
  388. expect(inner(root)).toBe('<div><p>0</p></div>')
  389. expect(block!.dynamicChildren!.length).toBe(1)
  390. expect(block!.dynamicChildren![0].type).toBe(Fragment)
  391. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  392. expect(
  393. serialize(
  394. block!.dynamicChildren![0].dynamicChildren![0].el as TestElement,
  395. ),
  396. ).toBe('<p>0</p>')
  397. foo.value++
  398. await nextTick()
  399. expect(inner(root)).toBe('<div><p>1</p></div>')
  400. })
  401. // #2169
  402. // block
  403. // - dynamic child (1)
  404. // - component (2)
  405. // When unmounting (1), we know we are in optimized mode so no need to further
  406. // traverse unmount its children
  407. test('should not perform unnecessary unmount traversals', () => {
  408. const spy = vi.fn()
  409. const Child = {
  410. setup() {
  411. onBeforeUnmount(spy)
  412. return () => 'child'
  413. },
  414. }
  415. const Parent = () => (
  416. openBlock(),
  417. createBlock('div', null, [
  418. createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */),
  419. ])
  420. )
  421. render(h(Parent), root)
  422. render(null, root)
  423. expect(spy).toHaveBeenCalledTimes(1)
  424. })
  425. // #2444
  426. // `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
  427. test('non-stable Fragment always need to diff its children', () => {
  428. const spyA = vi.fn()
  429. const spyB = vi.fn()
  430. const ChildA = {
  431. setup() {
  432. onBeforeUnmount(spyA)
  433. return () => 'child'
  434. },
  435. }
  436. const ChildB = {
  437. setup() {
  438. onBeforeUnmount(spyB)
  439. return () => 'child'
  440. },
  441. }
  442. const Parent = () => (
  443. openBlock(),
  444. createBlock('div', null, [
  445. (openBlock(true),
  446. createBlock(
  447. Fragment,
  448. null,
  449. [createVNode(ChildA, { key: 0 })],
  450. 128 /* KEYED_FRAGMENT */,
  451. )),
  452. (openBlock(true),
  453. createBlock(
  454. Fragment,
  455. null,
  456. [createVNode(ChildB)],
  457. 256 /* UNKEYED_FRAGMENT */,
  458. )),
  459. ])
  460. )
  461. render(h(Parent), root)
  462. render(null, root)
  463. expect(spyA).toHaveBeenCalledTimes(1)
  464. expect(spyB).toHaveBeenCalledTimes(1)
  465. })
  466. // #2893
  467. test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
  468. const state = ref(0)
  469. const CompA = {
  470. setup(props: any, { slots }: SetupContext) {
  471. return () => {
  472. return (
  473. openBlock(),
  474. createBlock('div', null, [renderSlot(slots, 'default')])
  475. )
  476. }
  477. },
  478. }
  479. const Wrapper = {
  480. setup(props: any, { slots }: SetupContext) {
  481. // use the manually written render function to rendering the optimized slots,
  482. // which should make subsequent updates exit the optimized mode correctly
  483. return () => {
  484. return slots.default!()[state.value]
  485. }
  486. },
  487. }
  488. const app = createApp({
  489. setup() {
  490. return () => {
  491. return (
  492. openBlock(),
  493. createBlock(Wrapper, null, {
  494. default: withCtx(() => [
  495. createVNode(CompA, null, {
  496. default: withCtx(() => [createTextVNode('Hello')]),
  497. _: 1 /* STABLE */,
  498. }),
  499. createVNode(CompA, null, {
  500. default: withCtx(() => [createTextVNode('World')]),
  501. _: 1 /* STABLE */,
  502. }),
  503. ]),
  504. _: 1 /* STABLE */,
  505. })
  506. )
  507. }
  508. },
  509. })
  510. app.mount(root)
  511. expect(inner(root)).toBe('<div>Hello</div>')
  512. state.value = 1
  513. await nextTick()
  514. expect(inner(root)).toBe('<div>World</div>')
  515. })
  516. //#3623
  517. test('nested teleport unmount need exit the optimization mode', () => {
  518. const target = nodeOps.createElement('div')
  519. const root = nodeOps.createElement('div')
  520. render(
  521. (openBlock(),
  522. createBlock('div', null, [
  523. (openBlock(),
  524. createBlock(
  525. Teleport as any,
  526. {
  527. to: target,
  528. },
  529. [
  530. createVNode('div', null, [
  531. (openBlock(),
  532. createBlock(
  533. Teleport as any,
  534. {
  535. to: target,
  536. },
  537. [createVNode('div', null, 'foo')],
  538. )),
  539. ]),
  540. ],
  541. )),
  542. ])),
  543. root,
  544. )
  545. expect(inner(target)).toMatchInlineSnapshot(
  546. `"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
  547. )
  548. expect(inner(root)).toMatchInlineSnapshot(
  549. `"<div><!--teleport start--><!--teleport end--></div>"`,
  550. )
  551. render(null, root)
  552. expect(inner(target)).toBe('')
  553. })
  554. // #3548
  555. test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
  556. const Comp = {
  557. setup(props: any, { slots }: SetupContext) {
  558. return () => {
  559. return (
  560. openBlock(),
  561. (block = createBlock('section', null, [
  562. renderSlot(slots, 'default'),
  563. ]))
  564. )
  565. }
  566. },
  567. }
  568. let dynamicVNode: VNode
  569. const Wrapper = {
  570. setup(props: any, { slots }: SetupContext) {
  571. return () => {
  572. return (
  573. openBlock(),
  574. createBlock(Comp, null, {
  575. default: withCtx(() => {
  576. return [
  577. (dynamicVNode = createVNode(
  578. 'div',
  579. {
  580. class: {
  581. foo: !!slots.default!(),
  582. },
  583. },
  584. null,
  585. PatchFlags.CLASS,
  586. )),
  587. ]
  588. }),
  589. _: 1,
  590. })
  591. )
  592. }
  593. },
  594. }
  595. const app = createApp({
  596. render() {
  597. return (
  598. openBlock(),
  599. createBlock(Wrapper, null, {
  600. default: withCtx(() => {
  601. return [createVNode({}) /* component */]
  602. }),
  603. _: 1,
  604. })
  605. )
  606. },
  607. })
  608. app.mount(root)
  609. expect(inner(root)).toBe('<section><div class="foo"></div></section>')
  610. /**
  611. * Block Tree:
  612. * - block(div)
  613. * - block(Fragment): renderSlots()
  614. * - dynamicVNode
  615. */
  616. expect(block!.dynamicChildren!.length).toBe(1)
  617. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  618. expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
  619. dynamicVNode!,
  620. )
  621. })
  622. // 3569
  623. test('should force bailout when the user manually calls the slot function', async () => {
  624. const index = ref(0)
  625. const Foo = {
  626. setup(props: any, { slots }: SetupContext) {
  627. return () => {
  628. return slots.default!()[index.value]
  629. }
  630. },
  631. }
  632. const app = createApp({
  633. setup() {
  634. return () => {
  635. return (
  636. openBlock(),
  637. createBlock(Foo, null, {
  638. default: withCtx(() => [
  639. true
  640. ? (openBlock(), createBlock('p', { key: 0 }, '1'))
  641. : createCommentVNode('v-if', true),
  642. true
  643. ? (openBlock(), createBlock('p', { key: 0 }, '2'))
  644. : createCommentVNode('v-if', true),
  645. ]),
  646. _: 1 /* STABLE */,
  647. })
  648. )
  649. }
  650. },
  651. })
  652. app.mount(root)
  653. expect(inner(root)).toBe('<p>1</p>')
  654. index.value = 1
  655. await nextTick()
  656. expect(inner(root)).toBe('<p>2</p>')
  657. index.value = 0
  658. await nextTick()
  659. expect(inner(root)).toBe('<p>1</p>')
  660. })
  661. // #3779
  662. test('treat slots manually written by the user as dynamic', async () => {
  663. const Middle = {
  664. setup(props: any, { slots }: any) {
  665. return slots.default!
  666. },
  667. }
  668. const Comp = {
  669. setup(props: any, { slots }: any) {
  670. return () => {
  671. return (
  672. openBlock(),
  673. createBlock('div', null, [
  674. createVNode(Middle, null, {
  675. default: withCtx(
  676. () => [
  677. createVNode('div', null, [renderSlot(slots, 'default')]),
  678. ],
  679. undefined,
  680. ),
  681. _: 3 /* FORWARDED */,
  682. }),
  683. ])
  684. )
  685. }
  686. },
  687. }
  688. const loading = ref(false)
  689. const app = createApp({
  690. setup() {
  691. return () => {
  692. // important: write the slot content here
  693. const content = h('span', loading.value ? 'loading' : 'loaded')
  694. return h(Comp, null, {
  695. default: () => content,
  696. })
  697. }
  698. },
  699. })
  700. app.mount(root)
  701. expect(inner(root)).toBe('<div><div><span>loaded</span></div></div>')
  702. loading.value = true
  703. await nextTick()
  704. expect(inner(root)).toBe('<div><div><span>loading</span></div></div>')
  705. })
  706. // #3828
  707. test('patch Suspense in optimized mode w/ nested dynamic nodes', async () => {
  708. const show = ref(false)
  709. const app = createApp({
  710. render() {
  711. return (
  712. openBlock(),
  713. createBlock(
  714. Fragment,
  715. null,
  716. [
  717. (openBlock(),
  718. createBlock(SuspenseImpl, null, {
  719. default: withCtx(() => [
  720. createVNode('div', null, [
  721. createVNode('div', null, show.value, PatchFlags.TEXT),
  722. ]),
  723. ]),
  724. _: SlotFlags.STABLE,
  725. })),
  726. ],
  727. PatchFlags.STABLE_FRAGMENT,
  728. )
  729. )
  730. },
  731. })
  732. app.mount(root)
  733. expect(inner(root)).toBe('<div><div>false</div></div>')
  734. show.value = true
  735. await nextTick()
  736. expect(inner(root)).toBe('<div><div>true</div></div>')
  737. })
  738. // #4183
  739. test('should not take unmount children fast path /w Suspense', async () => {
  740. const show = ref(true)
  741. const spyUnmounted = vi.fn()
  742. const Parent = {
  743. setup(props: any, { slots }: SetupContext) {
  744. return () => (
  745. openBlock(),
  746. createBlock(SuspenseImpl, null, {
  747. default: withCtx(() => [renderSlot(slots, 'default')]),
  748. _: SlotFlags.FORWARDED,
  749. })
  750. )
  751. },
  752. }
  753. const Child = {
  754. setup() {
  755. onUnmounted(spyUnmounted)
  756. return () => createVNode('div', null, show.value, PatchFlags.TEXT)
  757. },
  758. }
  759. const app = createApp({
  760. render() {
  761. return show.value
  762. ? (openBlock(),
  763. createBlock(
  764. Parent,
  765. { key: 0 },
  766. {
  767. default: withCtx(() => [createVNode(Child)]),
  768. _: SlotFlags.STABLE,
  769. },
  770. ))
  771. : createCommentVNode('v-if', true)
  772. },
  773. })
  774. app.mount(root)
  775. expect(inner(root)).toBe('<div>true</div>')
  776. show.value = false
  777. await nextTick()
  778. expect(inner(root)).toBe('<!--v-if-->')
  779. expect(spyUnmounted).toHaveBeenCalledTimes(1)
  780. })
  781. // #3881
  782. // root cause: fragment inside a compiled slot passed to component which
  783. // programmatically invokes the slot. The entire slot should de-opt but
  784. // the fragment was incorrectly put in optimized mode which causes it to skip
  785. // updates for its inner components.
  786. test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
  787. const Parent: FunctionalComponent = (_, { slots }) => slots.default!()
  788. const Dummy = () => 'dummy'
  789. const toggle = ref(true)
  790. const force = ref(0)
  791. const app = createApp({
  792. render() {
  793. if (!toggle.value) {
  794. return null
  795. }
  796. return h(
  797. Parent,
  798. { n: force.value },
  799. {
  800. default: withCtx(
  801. () => [
  802. createVNode('ul', null, [
  803. (openBlock(),
  804. createBlock(
  805. Fragment,
  806. null,
  807. renderList(1, item => {
  808. return createVNode('li', null, [createVNode(Dummy)])
  809. }),
  810. 64 /* STABLE_FRAGMENT */,
  811. )),
  812. ]),
  813. ],
  814. undefined,
  815. true,
  816. ),
  817. _: 1 /* STABLE */,
  818. },
  819. )
  820. },
  821. })
  822. app.mount(root)
  823. // force a patch
  824. force.value++
  825. await nextTick()
  826. expect(inner(root)).toBe(`<ul><li>dummy</li></ul>`)
  827. // unmount
  828. toggle.value = false
  829. await nextTick()
  830. // should successfully unmount without error
  831. expect(inner(root)).toBe(`<!---->`)
  832. })
  833. })