rendererOptimizedMode.spec.ts 21 KB

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