vdomFragment.spec.ts 8.1 KB

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