rendererOptimizedMode.spec.ts 21 KB

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