rendererFragment.spec.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import {
  2. h,
  3. createVNode,
  4. render,
  5. nodeOps,
  6. NodeTypes,
  7. TestElement,
  8. Fragment,
  9. resetOps,
  10. dumpOps,
  11. NodeOpTypes,
  12. serializeInner,
  13. createTextVNode
  14. } from '@vue/runtime-test'
  15. import { PatchFlags } from '@vue/shared'
  16. describe('renderer: fragment', () => {
  17. it('should allow returning multiple component root nodes', () => {
  18. const App = {
  19. render() {
  20. return [h('div', 'one'), 'two']
  21. }
  22. }
  23. const root = nodeOps.createElement('div')
  24. render(h(App), root)
  25. expect(serializeInner(root)).toBe(`<div>one</div>two`)
  26. expect(root.children.length).toBe(4)
  27. expect(root.children[0]).toMatchObject({
  28. type: NodeTypes.TEXT,
  29. text: ''
  30. })
  31. expect(root.children[1]).toMatchObject({
  32. type: NodeTypes.ELEMENT,
  33. tag: 'div'
  34. })
  35. expect((root.children[1] as TestElement).children[0]).toMatchObject({
  36. type: NodeTypes.TEXT,
  37. text: 'one'
  38. })
  39. expect(root.children[2]).toMatchObject({
  40. type: NodeTypes.TEXT,
  41. text: 'two'
  42. })
  43. expect(root.children[3]).toMatchObject({
  44. type: NodeTypes.TEXT,
  45. text: ''
  46. })
  47. })
  48. it('explicitly create fragments', () => {
  49. const root = nodeOps.createElement('div')
  50. render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root)
  51. const parent = root.children[0] as TestElement
  52. expect(serializeInner(parent)).toBe(`<div>one</div>two`)
  53. })
  54. it('patch fragment children (manual, keyed)', () => {
  55. const root = nodeOps.createElement('div')
  56. render(
  57. h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]),
  58. root
  59. )
  60. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  61. resetOps()
  62. render(
  63. h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]),
  64. root
  65. )
  66. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  67. const ops = dumpOps()
  68. // should be moving nodes instead of re-creating or patching them
  69. expect(ops).toMatchObject([
  70. {
  71. type: NodeOpTypes.INSERT
  72. }
  73. ])
  74. })
  75. it('patch fragment children (manual, unkeyed)', () => {
  76. const root = nodeOps.createElement('div')
  77. render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root)
  78. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  79. resetOps()
  80. render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root)
  81. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  82. const ops = dumpOps()
  83. // should be patching nodes instead of moving or re-creating them
  84. expect(ops).toMatchObject([
  85. {
  86. type: NodeOpTypes.SET_ELEMENT_TEXT
  87. },
  88. {
  89. type: NodeOpTypes.SET_ELEMENT_TEXT
  90. }
  91. ])
  92. })
  93. it('patch fragment children (compiler generated, unkeyed)', () => {
  94. const root = nodeOps.createElement('div')
  95. render(
  96. createVNode(
  97. Fragment,
  98. null,
  99. [
  100. createVNode('div', null, 'one', PatchFlags.TEXT),
  101. createTextVNode('two')
  102. ],
  103. PatchFlags.UNKEYED_FRAGMENT
  104. ),
  105. root
  106. )
  107. expect(serializeInner(root)).toBe(`<div>one</div>two`)
  108. render(
  109. createVNode(
  110. Fragment,
  111. null,
  112. [
  113. createVNode('div', null, 'foo', PatchFlags.TEXT),
  114. createTextVNode('bar'),
  115. createTextVNode('baz')
  116. ],
  117. PatchFlags.KEYED_FRAGMENT
  118. ),
  119. root
  120. )
  121. expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
  122. })
  123. it('patch fragment children (compiler generated, keyed)', () => {
  124. const root = nodeOps.createElement('div')
  125. render(
  126. createVNode(
  127. Fragment,
  128. null,
  129. [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')],
  130. PatchFlags.KEYED_FRAGMENT
  131. ),
  132. root
  133. )
  134. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  135. resetOps()
  136. render(
  137. createVNode(
  138. Fragment,
  139. null,
  140. [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')],
  141. PatchFlags.KEYED_FRAGMENT
  142. ),
  143. root
  144. )
  145. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  146. const ops = dumpOps()
  147. // should be moving nodes instead of re-creating or patching them
  148. expect(ops).toMatchObject([
  149. {
  150. type: NodeOpTypes.INSERT
  151. }
  152. ])
  153. })
  154. it('move fragment', () => {
  155. const root = nodeOps.createElement('div')
  156. render(
  157. h('div', [
  158. h('div', { key: 1 }, 'outer'),
  159. h(Fragment, { key: 2 }, [
  160. h('div', { key: 1 }, 'one'),
  161. h('div', { key: 2 }, 'two')
  162. ])
  163. ]),
  164. root
  165. )
  166. expect(serializeInner(root)).toBe(
  167. `<div><div>outer</div><div>one</div><div>two</div></div>`
  168. )
  169. resetOps()
  170. render(
  171. h('div', [
  172. h(Fragment, { key: 2 }, [
  173. h('div', { key: 2 }, 'two'),
  174. h('div', { key: 1 }, 'one')
  175. ]),
  176. h('div', { key: 1 }, 'outer')
  177. ]),
  178. root
  179. )
  180. expect(serializeInner(root)).toBe(
  181. `<div><div>two</div><div>one</div><div>outer</div></div>`
  182. )
  183. const ops = dumpOps()
  184. // should be moving nodes instead of re-creating them
  185. expect(ops).toMatchObject([
  186. // 1. re-order inside the fragment
  187. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  188. // 2. move entire fragment, including anchors
  189. // not the most efficient move, but this case is super rare
  190. // and optimizing for this special case complicates the algo quite a bit
  191. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  192. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  193. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  194. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
  195. ])
  196. })
  197. it('handle nested fragments', () => {
  198. const root = nodeOps.createElement('div')
  199. render(
  200. h(Fragment, [
  201. h('div', { key: 1 }, 'outer'),
  202. h(Fragment, { key: 2 }, [
  203. h('div', { key: 1 }, 'one'),
  204. h('div', { key: 2 }, 'two')
  205. ])
  206. ]),
  207. root
  208. )
  209. expect(serializeInner(root)).toBe(
  210. `<div>outer</div><div>one</div><div>two</div>`
  211. )
  212. resetOps()
  213. render(
  214. h(Fragment, [
  215. h(Fragment, { key: 2 }, [
  216. h('div', { key: 2 }, 'two'),
  217. h('div', { key: 1 }, 'one')
  218. ]),
  219. h('div', { key: 1 }, 'outer')
  220. ]),
  221. root
  222. )
  223. expect(serializeInner(root)).toBe(
  224. `<div>two</div><div>one</div><div>outer</div>`
  225. )
  226. const ops = dumpOps()
  227. // should be moving nodes instead of re-creating them
  228. expect(ops).toMatchObject([
  229. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  230. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  231. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  232. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  233. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
  234. ])
  235. // should properly remove nested fragments
  236. render(null, root)
  237. expect(serializeInner(root)).toBe(``)
  238. })
  239. })