rendererFragment.spec.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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.UNKEYED_FRAGMENT
  118. ),
  119. root
  120. )
  121. expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
  122. render(
  123. createVNode(
  124. Fragment,
  125. null,
  126. [
  127. createTextVNode('baz'),
  128. createVNode('div', null, 'foo', PatchFlags.TEXT)
  129. ],
  130. PatchFlags.UNKEYED_FRAGMENT
  131. ),
  132. root
  133. )
  134. expect(serializeInner(root)).toBe(`baz<div>foo</div>`)
  135. })
  136. it('patch fragment children (compiler generated, keyed)', () => {
  137. const root = nodeOps.createElement('div')
  138. render(
  139. createVNode(
  140. Fragment,
  141. null,
  142. [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')],
  143. PatchFlags.KEYED_FRAGMENT
  144. ),
  145. root
  146. )
  147. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  148. resetOps()
  149. render(
  150. createVNode(
  151. Fragment,
  152. null,
  153. [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')],
  154. PatchFlags.KEYED_FRAGMENT
  155. ),
  156. root
  157. )
  158. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  159. const ops = dumpOps()
  160. // should be moving nodes instead of re-creating or patching them
  161. expect(ops).toMatchObject([
  162. {
  163. type: NodeOpTypes.INSERT
  164. }
  165. ])
  166. })
  167. it('move fragment', () => {
  168. const root = nodeOps.createElement('div')
  169. render(
  170. h('div', [
  171. h('div', { key: 1 }, 'outer'),
  172. h(Fragment, { key: 2 }, [
  173. h('div', { key: 1 }, 'one'),
  174. h('div', { key: 2 }, 'two')
  175. ])
  176. ]),
  177. root
  178. )
  179. expect(serializeInner(root)).toBe(
  180. `<div><div>outer</div><div>one</div><div>two</div></div>`
  181. )
  182. resetOps()
  183. render(
  184. h('div', [
  185. h(Fragment, { key: 2 }, [
  186. h('div', { key: 2 }, 'two'),
  187. h('div', { key: 1 }, 'one')
  188. ]),
  189. h('div', { key: 1 }, 'outer')
  190. ]),
  191. root
  192. )
  193. expect(serializeInner(root)).toBe(
  194. `<div><div>two</div><div>one</div><div>outer</div></div>`
  195. )
  196. const ops = dumpOps()
  197. // should be moving nodes instead of re-creating them
  198. expect(ops).toMatchObject([
  199. // 1. re-order inside the fragment
  200. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  201. // 2. move entire fragment, including anchors
  202. // not the most efficient move, but this case is super rare
  203. // and optimizing for this special case complicates the algo quite a bit
  204. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  205. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  206. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  207. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
  208. ])
  209. })
  210. it('handle nested fragments', () => {
  211. const root = nodeOps.createElement('div')
  212. render(
  213. h(Fragment, [
  214. h('div', { key: 1 }, 'outer'),
  215. h(Fragment, { key: 2 }, [
  216. h('div', { key: 1 }, 'one'),
  217. h('div', { key: 2 }, 'two')
  218. ])
  219. ]),
  220. root
  221. )
  222. expect(serializeInner(root)).toBe(
  223. `<div>outer</div><div>one</div><div>two</div>`
  224. )
  225. resetOps()
  226. render(
  227. h(Fragment, [
  228. h(Fragment, { key: 2 }, [
  229. h('div', { key: 2 }, 'two'),
  230. h('div', { key: 1 }, 'one')
  231. ]),
  232. h('div', { key: 1 }, 'outer')
  233. ]),
  234. root
  235. )
  236. expect(serializeInner(root)).toBe(
  237. `<div>two</div><div>one</div><div>outer</div>`
  238. )
  239. const ops = dumpOps()
  240. // should be moving nodes instead of re-creating them
  241. expect(ops).toMatchObject([
  242. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  243. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  244. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  245. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  246. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
  247. ])
  248. // should properly remove nested fragments
  249. render(null, root)
  250. expect(serializeInner(root)).toBe(``)
  251. })
  252. })