rendererFragment.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import {
  2. Fragment,
  3. NodeOpTypes,
  4. type TestElement,
  5. TestNodeTypes,
  6. type VNode,
  7. createBlock,
  8. createCommentVNode,
  9. createTextVNode,
  10. createVNode,
  11. dumpOps,
  12. h,
  13. nodeOps,
  14. openBlock,
  15. render,
  16. resetOps,
  17. serializeInner,
  18. } from '@vue/runtime-test'
  19. import { PatchFlags } from '@vue/shared'
  20. import { renderList } from '../src/helpers/renderList'
  21. describe('renderer: fragment', () => {
  22. it('should allow returning multiple component root nodes', () => {
  23. const App = {
  24. render() {
  25. return [h('div', 'one'), 'two']
  26. },
  27. }
  28. const root = nodeOps.createElement('div')
  29. render(h(App), root)
  30. expect(serializeInner(root)).toBe(`<div>one</div>two`)
  31. expect(root.children.length).toBe(4)
  32. expect(root.children[0]).toMatchObject({
  33. type: TestNodeTypes.TEXT,
  34. text: '',
  35. })
  36. expect(root.children[1]).toMatchObject({
  37. type: TestNodeTypes.ELEMENT,
  38. tag: 'div',
  39. })
  40. expect((root.children[1] as TestElement).children[0]).toMatchObject({
  41. type: TestNodeTypes.TEXT,
  42. text: 'one',
  43. })
  44. expect(root.children[2]).toMatchObject({
  45. type: TestNodeTypes.TEXT,
  46. text: 'two',
  47. })
  48. expect(root.children[3]).toMatchObject({
  49. type: TestNodeTypes.TEXT,
  50. text: '',
  51. })
  52. })
  53. it('explicitly create fragments', () => {
  54. const root = nodeOps.createElement('div')
  55. render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root)
  56. const parent = root.children[0] as TestElement
  57. expect(serializeInner(parent)).toBe(`<div>one</div>two`)
  58. })
  59. it('patch fragment children (manual, keyed)', () => {
  60. const root = nodeOps.createElement('div')
  61. render(
  62. h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]),
  63. root,
  64. )
  65. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  66. resetOps()
  67. render(
  68. h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]),
  69. root,
  70. )
  71. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  72. const ops = dumpOps()
  73. // should be moving nodes instead of re-creating or patching them
  74. expect(ops).toMatchObject([
  75. {
  76. type: NodeOpTypes.INSERT,
  77. },
  78. ])
  79. })
  80. it('patch fragment children (manual, unkeyed)', () => {
  81. const root = nodeOps.createElement('div')
  82. render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root)
  83. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  84. resetOps()
  85. render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root)
  86. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  87. const ops = dumpOps()
  88. // should be patching nodes instead of moving or re-creating them
  89. expect(ops).toMatchObject([
  90. {
  91. type: NodeOpTypes.SET_ELEMENT_TEXT,
  92. },
  93. {
  94. type: NodeOpTypes.SET_ELEMENT_TEXT,
  95. },
  96. ])
  97. })
  98. it('patch fragment children (compiler generated, unkeyed)', () => {
  99. const root = nodeOps.createElement('div')
  100. render(
  101. createVNode(
  102. Fragment,
  103. null,
  104. [
  105. createVNode('div', null, 'one', PatchFlags.TEXT),
  106. createTextVNode('two'),
  107. ],
  108. PatchFlags.UNKEYED_FRAGMENT,
  109. ),
  110. root,
  111. )
  112. expect(serializeInner(root)).toBe(`<div>one</div>two`)
  113. render(
  114. createVNode(
  115. Fragment,
  116. null,
  117. [
  118. createVNode('div', null, 'foo', PatchFlags.TEXT),
  119. createTextVNode('bar'),
  120. createTextVNode('baz'),
  121. ],
  122. PatchFlags.UNKEYED_FRAGMENT,
  123. ),
  124. root,
  125. )
  126. expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
  127. render(
  128. createVNode(
  129. Fragment,
  130. null,
  131. [
  132. createTextVNode('baz'),
  133. createVNode('div', null, 'foo', PatchFlags.TEXT),
  134. ],
  135. PatchFlags.UNKEYED_FRAGMENT,
  136. ),
  137. root,
  138. )
  139. expect(serializeInner(root)).toBe(`baz<div>foo</div>`)
  140. })
  141. it('patch fragment children (compiler generated, keyed)', () => {
  142. const root = nodeOps.createElement('div')
  143. render(
  144. createVNode(
  145. Fragment,
  146. null,
  147. [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')],
  148. PatchFlags.KEYED_FRAGMENT,
  149. ),
  150. root,
  151. )
  152. expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
  153. resetOps()
  154. render(
  155. createVNode(
  156. Fragment,
  157. null,
  158. [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')],
  159. PatchFlags.KEYED_FRAGMENT,
  160. ),
  161. root,
  162. )
  163. expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
  164. const ops = dumpOps()
  165. // should be moving nodes instead of re-creating or patching them
  166. expect(ops).toMatchObject([
  167. {
  168. type: NodeOpTypes.INSERT,
  169. },
  170. ])
  171. })
  172. it('move fragment', () => {
  173. const root = nodeOps.createElement('div')
  174. render(
  175. h('div', [
  176. h('div', { key: 1 }, 'outer'),
  177. h(Fragment, { key: 2 }, [
  178. h('div', { key: 1 }, 'one'),
  179. h('div', { key: 2 }, 'two'),
  180. ]),
  181. ]),
  182. root,
  183. )
  184. expect(serializeInner(root)).toBe(
  185. `<div><div>outer</div><div>one</div><div>two</div></div>`,
  186. )
  187. resetOps()
  188. render(
  189. h('div', [
  190. h(Fragment, { key: 2 }, [
  191. h('div', { key: 2 }, 'two'),
  192. h('div', { key: 1 }, 'one'),
  193. ]),
  194. h('div', { key: 1 }, 'outer'),
  195. ]),
  196. root,
  197. )
  198. expect(serializeInner(root)).toBe(
  199. `<div><div>two</div><div>one</div><div>outer</div></div>`,
  200. )
  201. const ops = dumpOps()
  202. // should be moving nodes instead of re-creating them
  203. expect(ops).toMatchObject([
  204. // 1. re-order inside the fragment
  205. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  206. // 2. move entire fragment, including anchors
  207. // not the most efficient move, but this case is super rare
  208. // and optimizing for this special case complicates the algo quite a bit
  209. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  210. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  211. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  212. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  213. ])
  214. })
  215. it('handle nested fragments', () => {
  216. const root = nodeOps.createElement('div')
  217. render(
  218. h(Fragment, [
  219. h('div', { key: 1 }, 'outer'),
  220. h(Fragment, { key: 2 }, [
  221. h('div', { key: 1 }, 'one'),
  222. h('div', { key: 2 }, 'two'),
  223. ]),
  224. ]),
  225. root,
  226. )
  227. expect(serializeInner(root)).toBe(
  228. `<div>outer</div><div>one</div><div>two</div>`,
  229. )
  230. resetOps()
  231. render(
  232. h(Fragment, [
  233. h(Fragment, { key: 2 }, [
  234. h('div', { key: 2 }, 'two'),
  235. h('div', { key: 1 }, 'one'),
  236. ]),
  237. h('div', { key: 1 }, 'outer'),
  238. ]),
  239. root,
  240. )
  241. expect(serializeInner(root)).toBe(
  242. `<div>two</div><div>one</div><div>outer</div>`,
  243. )
  244. const ops = dumpOps()
  245. // should be moving nodes instead of re-creating them
  246. expect(ops).toMatchObject([
  247. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  248. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  249. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  250. { type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
  251. { type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
  252. ])
  253. // should properly remove nested fragments
  254. render(null, root)
  255. expect(serializeInner(root)).toBe(``)
  256. })
  257. // #2080
  258. test('`template` keyed fragment w/ comment + hoisted node', () => {
  259. const root = nodeOps.createElement('div')
  260. const hoisted = h('span')
  261. const renderFn = (items: string[]) => {
  262. return (
  263. openBlock(true),
  264. createBlock(
  265. Fragment,
  266. null,
  267. renderList(items, item => {
  268. return (
  269. openBlock(),
  270. createBlock(
  271. Fragment,
  272. { key: item },
  273. [
  274. createCommentVNode('comment'),
  275. hoisted,
  276. createVNode('div', null, item, PatchFlags.TEXT),
  277. ],
  278. PatchFlags.STABLE_FRAGMENT,
  279. )
  280. )
  281. }),
  282. PatchFlags.KEYED_FRAGMENT,
  283. )
  284. )
  285. }
  286. render(renderFn(['one', 'two']), root)
  287. expect(serializeInner(root)).toBe(
  288. `<!--comment--><span></span><div>one</div><!--comment--><span></span><div>two</div>`,
  289. )
  290. render(renderFn(['two', 'one']), root)
  291. expect(serializeInner(root)).toBe(
  292. `<!--comment--><span></span><div>two</div><!--comment--><span></span><div>one</div>`,
  293. )
  294. })
  295. // #10547
  296. test('`template` fragment w/ render function', () => {
  297. const renderFn = (vnode: VNode) => {
  298. return (
  299. openBlock(),
  300. createBlock(
  301. Fragment,
  302. null,
  303. [createTextVNode('text'), (openBlock(), createBlock(vnode))],
  304. PatchFlags.STABLE_FRAGMENT,
  305. )
  306. )
  307. }
  308. const root = nodeOps.createElement('div')
  309. const foo = h('div', ['foo'])
  310. const bar = h('div', [h('div', 'bar')])
  311. render(renderFn(foo), root)
  312. expect(serializeInner(root)).toBe(`text<div>foo</div>`)
  313. render(renderFn(bar), root)
  314. expect(serializeInner(root)).toBe(`text<div><div>bar</div></div>`)
  315. render(renderFn(foo), root)
  316. expect(serializeInner(root)).toBe(`text<div>foo</div>`)
  317. })
  318. // #10547
  319. test('`template` fragment w/ render function + keyed vnode', () => {
  320. const renderFn = (vnode: VNode) => {
  321. return (
  322. openBlock(),
  323. createBlock(
  324. Fragment,
  325. null,
  326. [createTextVNode('text'), (openBlock(), createBlock(vnode))],
  327. PatchFlags.STABLE_FRAGMENT,
  328. )
  329. )
  330. }
  331. const root = nodeOps.createElement('div')
  332. const foo = h('div', { key: 1 }, [h('div', 'foo')])
  333. const bar = h('div', { key: 2 }, [h('div', 'bar'), h('div', 'bar')])
  334. render(renderFn(foo), root)
  335. expect(serializeInner(root)).toBe(`text<div><div>foo</div></div>`)
  336. render(renderFn(bar), root)
  337. expect(serializeInner(root)).toBe(
  338. `text<div><div>bar</div><div>bar</div></div>`,
  339. )
  340. render(renderFn(foo), root)
  341. expect(serializeInner(root)).toBe(`text<div><div>foo</div></div>`)
  342. })
  343. // #6852
  344. test('`template` keyed fragment w/ text', () => {
  345. const root = nodeOps.createElement('div')
  346. const renderFn = (items: string[]) => {
  347. return (
  348. openBlock(true),
  349. createBlock(
  350. Fragment,
  351. null,
  352. renderList(items, item => {
  353. return (
  354. openBlock(),
  355. createBlock(
  356. Fragment,
  357. { key: item },
  358. [
  359. createTextVNode('text'),
  360. createVNode('div', null, item, PatchFlags.TEXT),
  361. ],
  362. PatchFlags.STABLE_FRAGMENT,
  363. )
  364. )
  365. }),
  366. PatchFlags.KEYED_FRAGMENT,
  367. )
  368. )
  369. }
  370. render(renderFn(['one', 'two']), root)
  371. expect(serializeInner(root)).toBe(`text<div>one</div>text<div>two</div>`)
  372. render(renderFn(['two', 'one']), root)
  373. expect(serializeInner(root)).toBe(`text<div>two</div>text<div>one</div>`)
  374. })
  375. // #10007
  376. test('empty fragment', () => {
  377. const root = nodeOps.createElement('div')
  378. const renderFn = () => {
  379. return (openBlock(true), createBlock(Fragment, null))
  380. }
  381. render(renderFn(), root)
  382. expect(serializeInner(root)).toBe('')
  383. })
  384. })