rendererFragment.spec.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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. createBlock,
  15. openBlock,
  16. createCommentVNode
  17. } from '@vue/runtime-test'
  18. import { PatchFlags } from '@vue/shared'
  19. import { renderList } from '../src/helpers/renderList'
  20. describe('renderer: fragment', () => {
  21. it('should allow returning multiple component root nodes', () => {
  22. const App = {
  23. render() {
  24. return [h('div', 'one'), 'two']
  25. }
  26. }
  27. const root = nodeOps.createElement('div')
  28. render(h(App), root)
  29. expect(serializeInner(root)).toBe(`<div>one</div>two`)
  30. expect(root.children.length).toBe(4)
  31. expect(root.children[0]).toMatchObject({
  32. type: NodeTypes.TEXT,
  33. text: ''
  34. })
  35. expect(root.children[1]).toMatchObject({
  36. type: NodeTypes.ELEMENT,
  37. tag: 'div'
  38. })
  39. expect((root.children[1] as TestElement).children[0]).toMatchObject({
  40. type: NodeTypes.TEXT,
  41. text: 'one'
  42. })
  43. expect(root.children[2]).toMatchObject({
  44. type: NodeTypes.TEXT,
  45. text: 'two'
  46. })
  47. expect(root.children[3]).toMatchObject({
  48. type: NodeTypes.TEXT,
  49. text: ''
  50. })
  51. })
  52. it('explicitly create fragments', () => {
  53. const root = nodeOps.createElement('div')
  54. render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root)
  55. const parent = root.children[0] as TestElement
  56. expect(serializeInner(parent)).toBe(`<div>one</div>two`)
  57. })
  58. it('patch fragment children (manual, keyed)', () => {
  59. const root = nodeOps.createElement('div')
  60. render(
  61. h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]),
  62. root
  63. )
  64. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  65. resetOps()
  66. render(
  67. h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]),
  68. root
  69. )
  70. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  71. const ops = dumpOps()
  72. // should be moving nodes instead of re-creating or patching them
  73. expect(ops).toMatchObject([
  74. {
  75. type: NodeOpTypes.INSERT
  76. }
  77. ])
  78. })
  79. it('patch fragment children (manual, unkeyed)', () => {
  80. const root = nodeOps.createElement('div')
  81. render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root)
  82. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  83. resetOps()
  84. render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root)
  85. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  86. const ops = dumpOps()
  87. // should be patching nodes instead of moving or re-creating them
  88. expect(ops).toMatchObject([
  89. {
  90. type: NodeOpTypes.SET_ELEMENT_TEXT
  91. },
  92. {
  93. type: NodeOpTypes.SET_ELEMENT_TEXT
  94. }
  95. ])
  96. })
  97. it('patch fragment children (compiler generated, unkeyed)', () => {
  98. const root = nodeOps.createElement('div')
  99. render(
  100. createVNode(
  101. Fragment,
  102. null,
  103. [
  104. createVNode('div', null, 'one', PatchFlags.TEXT),
  105. createTextVNode('two')
  106. ],
  107. PatchFlags.UNKEYED_FRAGMENT
  108. ),
  109. root
  110. )
  111. expect(serializeInner(root)).toBe(`<div>one</div>two`)
  112. render(
  113. createVNode(
  114. Fragment,
  115. null,
  116. [
  117. createVNode('div', null, 'foo', PatchFlags.TEXT),
  118. createTextVNode('bar'),
  119. createTextVNode('baz')
  120. ],
  121. PatchFlags.UNKEYED_FRAGMENT
  122. ),
  123. root
  124. )
  125. expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
  126. render(
  127. createVNode(
  128. Fragment,
  129. null,
  130. [
  131. createTextVNode('baz'),
  132. createVNode('div', null, 'foo', PatchFlags.TEXT)
  133. ],
  134. PatchFlags.UNKEYED_FRAGMENT
  135. ),
  136. root
  137. )
  138. expect(serializeInner(root)).toBe(`baz<div>foo</div>`)
  139. })
  140. it('patch fragment children (compiler generated, keyed)', () => {
  141. const root = nodeOps.createElement('div')
  142. render(
  143. createVNode(
  144. Fragment,
  145. null,
  146. [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')],
  147. PatchFlags.KEYED_FRAGMENT
  148. ),
  149. root
  150. )
  151. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  152. resetOps()
  153. render(
  154. createVNode(
  155. Fragment,
  156. null,
  157. [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')],
  158. PatchFlags.KEYED_FRAGMENT
  159. ),
  160. root
  161. )
  162. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  163. const ops = dumpOps()
  164. // should be moving nodes instead of re-creating or patching them
  165. expect(ops).toMatchObject([
  166. {
  167. type: NodeOpTypes.INSERT
  168. }
  169. ])
  170. })
  171. it('move fragment', () => {
  172. const root = nodeOps.createElement('div')
  173. render(
  174. h('div', [
  175. h('div', { key: 1 }, 'outer'),
  176. h(Fragment, { key: 2 }, [
  177. h('div', { key: 1 }, 'one'),
  178. h('div', { key: 2 }, 'two')
  179. ])
  180. ]),
  181. root
  182. )
  183. expect(serializeInner(root)).toBe(
  184. `<div><div>outer</div><div>one</div><div>two</div></div>`
  185. )
  186. resetOps()
  187. render(
  188. h('div', [
  189. h(Fragment, { key: 2 }, [
  190. h('div', { key: 2 }, 'two'),
  191. h('div', { key: 1 }, 'one')
  192. ]),
  193. h('div', { key: 1 }, 'outer')
  194. ]),
  195. root
  196. )
  197. expect(serializeInner(root)).toBe(
  198. `<div><div>two</div><div>one</div><div>outer</div></div>`
  199. )
  200. const ops = dumpOps()
  201. // should be moving nodes instead of re-creating them
  202. expect(ops).toMatchObject([
  203. // 1. re-order inside the fragment
  204. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  205. // 2. move entire fragment, including anchors
  206. // not the most efficient move, but this case is super rare
  207. // and optimizing for this special case complicates the algo quite a bit
  208. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  209. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  210. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  211. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
  212. ])
  213. })
  214. it('handle nested fragments', () => {
  215. const root = nodeOps.createElement('div')
  216. render(
  217. h(Fragment, [
  218. h('div', { key: 1 }, 'outer'),
  219. h(Fragment, { key: 2 }, [
  220. h('div', { key: 1 }, 'one'),
  221. h('div', { key: 2 }, 'two')
  222. ])
  223. ]),
  224. root
  225. )
  226. expect(serializeInner(root)).toBe(
  227. `<div>outer</div><div>one</div><div>two</div>`
  228. )
  229. resetOps()
  230. render(
  231. h(Fragment, [
  232. h(Fragment, { key: 2 }, [
  233. h('div', { key: 2 }, 'two'),
  234. h('div', { key: 1 }, 'one')
  235. ]),
  236. h('div', { key: 1 }, 'outer')
  237. ]),
  238. root
  239. )
  240. expect(serializeInner(root)).toBe(
  241. `<div>two</div><div>one</div><div>outer</div>`
  242. )
  243. const ops = dumpOps()
  244. // should be moving nodes instead of re-creating them
  245. expect(ops).toMatchObject([
  246. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  247. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  248. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  249. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  250. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
  251. ])
  252. // should properly remove nested fragments
  253. render(null, root)
  254. expect(serializeInner(root)).toBe(``)
  255. })
  256. // #2080
  257. test('`template` keyed fragment w/ comment + hoisted node', () => {
  258. const root = nodeOps.createElement('div')
  259. const hoisted = h('span')
  260. const renderFn = (items: string[]) => {
  261. return (
  262. openBlock(true),
  263. createBlock(
  264. Fragment,
  265. null,
  266. renderList(items, item => {
  267. return (
  268. openBlock(),
  269. createBlock(
  270. Fragment,
  271. { key: item },
  272. [
  273. createCommentVNode('comment'),
  274. hoisted,
  275. createVNode('div', null, item, PatchFlags.TEXT)
  276. ],
  277. PatchFlags.STABLE_FRAGMENT
  278. )
  279. )
  280. }),
  281. PatchFlags.KEYED_FRAGMENT
  282. )
  283. )
  284. }
  285. render(renderFn(['one', 'two']), root)
  286. expect(serializeInner(root)).toBe(
  287. `<!--comment--><span></span><div>one</div><!--comment--><span></span><div>two</div>`
  288. )
  289. render(renderFn(['two', 'one']), root)
  290. expect(serializeInner(root)).toBe(
  291. `<!--comment--><span></span><div>two</div><!--comment--><span></span><div>one</div>`
  292. )
  293. })
  294. })