rendererChildren.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. // reference: https://github.com/vuejs/vue/blob/dev/test/unit/modules/vdom/patch/children.spec.js
  2. import {
  3. type TestElement,
  4. TestNodeTypes,
  5. h,
  6. nodeOps,
  7. render,
  8. serialize,
  9. serializeInner,
  10. } from '@vue/runtime-test'
  11. function toSpan(content: any) {
  12. if (typeof content === 'string') {
  13. return h('span', content.toString())
  14. } else {
  15. return h('span', { key: content }, content.toString())
  16. }
  17. }
  18. const inner = (c: TestElement) => serializeInner(c)
  19. function shuffle(array: Array<any>) {
  20. let currentIndex = array.length
  21. let temporaryValue
  22. let randomIndex
  23. // while there remain elements to shuffle...
  24. while (currentIndex !== 0) {
  25. // pick a remaining element...
  26. randomIndex = Math.floor(Math.random() * currentIndex)
  27. currentIndex -= 1
  28. // and swap it with the current element.
  29. temporaryValue = array[currentIndex]
  30. array[currentIndex] = array[randomIndex]
  31. array[randomIndex] = temporaryValue
  32. }
  33. return array
  34. }
  35. test('should patch previously empty children', () => {
  36. const root = nodeOps.createElement('div')
  37. render(h('div', []), root)
  38. expect(inner(root)).toBe('<div></div>')
  39. render(h('div', ['hello']), root)
  40. expect(inner(root)).toBe('<div>hello</div>')
  41. })
  42. test('should patch previously null children', () => {
  43. const root = nodeOps.createElement('div')
  44. render(h('div'), root)
  45. expect(inner(root)).toBe('<div></div>')
  46. render(h('div', ['hello']), root)
  47. expect(inner(root)).toBe('<div>hello</div>')
  48. })
  49. test('array children -> text children', () => {
  50. const root = nodeOps.createElement('div')
  51. render(h('div', [h('div')]), root)
  52. expect(inner(root)).toBe('<div><div></div></div>')
  53. render(h('div', 'hello'), root)
  54. expect(inner(root)).toBe('<div>hello</div>')
  55. })
  56. test('plain object child', () => {
  57. const root = nodeOps.createElement('div')
  58. const foo = { foo: '1' }
  59. // @ts-expect-error
  60. render(h('div', null, [foo]), root)
  61. expect('Invalid VNode type').not.toHaveBeenWarned()
  62. expect(inner(root)).toBe('<div>[object Object]</div>')
  63. })
  64. describe('renderer: keyed children', () => {
  65. let root: TestElement
  66. let elm: TestElement
  67. const renderChildren = (arr: number[]) => {
  68. render(h('div', arr.map(toSpan)), root)
  69. return root.children[0] as TestElement
  70. }
  71. beforeEach(() => {
  72. root = nodeOps.createElement('div')
  73. render(h('div', { id: 1 }, 'hello'), root)
  74. })
  75. test('append', () => {
  76. elm = renderChildren([1])
  77. expect(elm.children.length).toBe(1)
  78. elm = renderChildren([1, 2, 3])
  79. expect(elm.children.length).toBe(3)
  80. expect(serialize(elm.children[1])).toBe('<span>2</span>')
  81. expect(serialize(elm.children[2])).toBe('<span>3</span>')
  82. })
  83. test('prepend', () => {
  84. elm = renderChildren([4, 5])
  85. expect(elm.children.length).toBe(2)
  86. elm = renderChildren([1, 2, 3, 4, 5])
  87. expect(elm.children.length).toBe(5)
  88. expect((elm.children as TestElement[]).map(inner)).toEqual([
  89. '1',
  90. '2',
  91. '3',
  92. '4',
  93. '5',
  94. ])
  95. })
  96. test('insert in middle', () => {
  97. elm = renderChildren([1, 2, 4, 5])
  98. expect(elm.children.length).toBe(4)
  99. elm = renderChildren([1, 2, 3, 4, 5])
  100. expect(elm.children.length).toBe(5)
  101. expect((elm.children as TestElement[]).map(inner)).toEqual([
  102. '1',
  103. '2',
  104. '3',
  105. '4',
  106. '5',
  107. ])
  108. })
  109. test('insert at beginning and end', () => {
  110. elm = renderChildren([2, 3, 4])
  111. expect(elm.children.length).toBe(3)
  112. elm = renderChildren([1, 2, 3, 4, 5])
  113. expect(elm.children.length).toBe(5)
  114. expect((elm.children as TestElement[]).map(inner)).toEqual([
  115. '1',
  116. '2',
  117. '3',
  118. '4',
  119. '5',
  120. ])
  121. })
  122. test('insert to empty parent', () => {
  123. elm = renderChildren([])
  124. expect(elm.children.length).toBe(0)
  125. elm = renderChildren([1, 2, 3, 4, 5])
  126. expect(elm.children.length).toBe(5)
  127. expect((elm.children as TestElement[]).map(inner)).toEqual([
  128. '1',
  129. '2',
  130. '3',
  131. '4',
  132. '5',
  133. ])
  134. })
  135. test('remove all children from parent', () => {
  136. elm = renderChildren([1, 2, 3, 4, 5])
  137. expect(elm.children.length).toBe(5)
  138. expect((elm.children as TestElement[]).map(inner)).toEqual([
  139. '1',
  140. '2',
  141. '3',
  142. '4',
  143. '5',
  144. ])
  145. render(h('div'), root)
  146. expect(elm.children.length).toBe(0)
  147. })
  148. test('remove from beginning', () => {
  149. elm = renderChildren([1, 2, 3, 4, 5])
  150. expect(elm.children.length).toBe(5)
  151. elm = renderChildren([3, 4, 5])
  152. expect(elm.children.length).toBe(3)
  153. expect((elm.children as TestElement[]).map(inner)).toEqual(['3', '4', '5'])
  154. })
  155. test('remove from end', () => {
  156. elm = renderChildren([1, 2, 3, 4, 5])
  157. expect(elm.children.length).toBe(5)
  158. elm = renderChildren([1, 2, 3])
  159. expect(elm.children.length).toBe(3)
  160. expect((elm.children as TestElement[]).map(inner)).toEqual(['1', '2', '3'])
  161. })
  162. test('remove from middle', () => {
  163. elm = renderChildren([1, 2, 3, 4, 5])
  164. expect(elm.children.length).toBe(5)
  165. elm = renderChildren([1, 2, 4, 5])
  166. expect(elm.children.length).toBe(4)
  167. expect((elm.children as TestElement[]).map(inner)).toEqual([
  168. '1',
  169. '2',
  170. '4',
  171. '5',
  172. ])
  173. })
  174. test('moving single child forward', () => {
  175. elm = renderChildren([1, 2, 3, 4])
  176. expect(elm.children.length).toBe(4)
  177. elm = renderChildren([2, 3, 1, 4])
  178. expect(elm.children.length).toBe(4)
  179. expect((elm.children as TestElement[]).map(inner)).toEqual([
  180. '2',
  181. '3',
  182. '1',
  183. '4',
  184. ])
  185. })
  186. test('moving single child backwards', () => {
  187. elm = renderChildren([1, 2, 3, 4])
  188. expect(elm.children.length).toBe(4)
  189. elm = renderChildren([1, 4, 2, 3])
  190. expect(elm.children.length).toBe(4)
  191. expect((elm.children as TestElement[]).map(inner)).toEqual([
  192. '1',
  193. '4',
  194. '2',
  195. '3',
  196. ])
  197. })
  198. test('moving single child to end', () => {
  199. elm = renderChildren([1, 2, 3])
  200. expect(elm.children.length).toBe(3)
  201. elm = renderChildren([2, 3, 1])
  202. expect(elm.children.length).toBe(3)
  203. expect((elm.children as TestElement[]).map(inner)).toEqual(['2', '3', '1'])
  204. })
  205. test('swap first and last', () => {
  206. elm = renderChildren([1, 2, 3, 4])
  207. expect(elm.children.length).toBe(4)
  208. elm = renderChildren([4, 2, 3, 1])
  209. expect(elm.children.length).toBe(4)
  210. expect((elm.children as TestElement[]).map(inner)).toEqual([
  211. '4',
  212. '2',
  213. '3',
  214. '1',
  215. ])
  216. })
  217. test('move to left & replace', () => {
  218. elm = renderChildren([1, 2, 3, 4, 5])
  219. expect(elm.children.length).toBe(5)
  220. elm = renderChildren([4, 1, 2, 3, 6])
  221. expect(elm.children.length).toBe(5)
  222. expect((elm.children as TestElement[]).map(inner)).toEqual([
  223. '4',
  224. '1',
  225. '2',
  226. '3',
  227. '6',
  228. ])
  229. })
  230. test('move to left and leaves hold', () => {
  231. elm = renderChildren([1, 4, 5])
  232. expect(elm.children.length).toBe(3)
  233. elm = renderChildren([4, 6])
  234. expect((elm.children as TestElement[]).map(inner)).toEqual(['4', '6'])
  235. })
  236. test('moved and set to undefined element ending at the end', () => {
  237. elm = renderChildren([2, 4, 5])
  238. expect(elm.children.length).toBe(3)
  239. elm = renderChildren([4, 5, 3])
  240. expect(elm.children.length).toBe(3)
  241. expect((elm.children as TestElement[]).map(inner)).toEqual(['4', '5', '3'])
  242. })
  243. test('reverse element', () => {
  244. elm = renderChildren([1, 2, 3, 4, 5, 6, 7, 8])
  245. expect(elm.children.length).toBe(8)
  246. elm = renderChildren([8, 7, 6, 5, 4, 3, 2, 1])
  247. expect((elm.children as TestElement[]).map(inner)).toEqual([
  248. '8',
  249. '7',
  250. '6',
  251. '5',
  252. '4',
  253. '3',
  254. '2',
  255. '1',
  256. ])
  257. })
  258. test('something', () => {
  259. elm = renderChildren([0, 1, 2, 3, 4, 5])
  260. expect(elm.children.length).toBe(6)
  261. elm = renderChildren([4, 3, 2, 1, 5, 0])
  262. expect((elm.children as TestElement[]).map(inner)).toEqual([
  263. '4',
  264. '3',
  265. '2',
  266. '1',
  267. '5',
  268. '0',
  269. ])
  270. })
  271. test('random shuffle', () => {
  272. const elms = 14
  273. const samples = 5
  274. const arr = [...Array(elms).keys()]
  275. const opacities: string[] = []
  276. function spanNumWithOpacity(n: number, o: string) {
  277. return h('span', { key: n, style: { opacity: o } }, n.toString())
  278. }
  279. for (let n = 0; n < samples; ++n) {
  280. render(
  281. h(
  282. 'span',
  283. arr.map(n => spanNumWithOpacity(n, '1')),
  284. ),
  285. root,
  286. )
  287. elm = root.children[0] as TestElement
  288. for (let i = 0; i < elms; ++i) {
  289. expect(serializeInner(elm.children[i] as TestElement)).toBe(
  290. i.toString(),
  291. )
  292. opacities[i] = Math.random().toFixed(5).toString()
  293. }
  294. const shufArr = shuffle(arr.slice(0))
  295. render(
  296. h(
  297. 'span',
  298. arr.map(n => spanNumWithOpacity(shufArr[n], opacities[n])),
  299. ),
  300. root,
  301. )
  302. elm = root.children[0] as TestElement
  303. for (let i = 0; i < elms; ++i) {
  304. expect(serializeInner(elm.children[i] as TestElement)).toBe(
  305. shufArr[i].toString(),
  306. )
  307. expect(elm.children[i]).toMatchObject({
  308. props: {
  309. style: {
  310. opacity: opacities[i],
  311. },
  312. },
  313. })
  314. }
  315. }
  316. })
  317. test('children with the same key but with different tag', () => {
  318. render(
  319. h('div', [
  320. h('div', { key: 1 }, 'one'),
  321. h('div', { key: 2 }, 'two'),
  322. h('div', { key: 3 }, 'three'),
  323. h('div', { key: 4 }, 'four'),
  324. ]),
  325. root,
  326. )
  327. elm = root.children[0] as TestElement
  328. expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
  329. 'div',
  330. 'div',
  331. 'div',
  332. 'div',
  333. ])
  334. expect((elm.children as TestElement[]).map(inner)).toEqual([
  335. 'one',
  336. 'two',
  337. 'three',
  338. 'four',
  339. ])
  340. render(
  341. h('div', [
  342. h('div', { key: 4 }, 'four'),
  343. h('span', { key: 3 }, 'three'),
  344. h('span', { key: 2 }, 'two'),
  345. h('div', { key: 1 }, 'one'),
  346. ]),
  347. root,
  348. )
  349. expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
  350. 'div',
  351. 'span',
  352. 'span',
  353. 'div',
  354. ])
  355. expect((elm.children as TestElement[]).map(inner)).toEqual([
  356. 'four',
  357. 'three',
  358. 'two',
  359. 'one',
  360. ])
  361. })
  362. test('children with the same tag, same key, but one with data and one without data', () => {
  363. render(h('div', [h('div', { class: 'hi' }, 'one')]), root)
  364. elm = root.children[0] as TestElement
  365. expect(elm.children[0]).toMatchObject({
  366. props: {
  367. class: 'hi',
  368. },
  369. })
  370. render(h('div', [h('div', 'four')]), root)
  371. elm = root.children[0] as TestElement
  372. expect(elm.children[0] as TestElement).toMatchObject({
  373. props: {
  374. // in the DOM renderer this will be ''
  375. // but the test renderer simply sets whatever value it receives.
  376. class: null,
  377. },
  378. })
  379. expect(serialize(elm.children[0])).toBe(`<div>four</div>`)
  380. })
  381. test('should warn with duplicate keys', () => {
  382. renderChildren([1, 2, 3, 4, 5])
  383. renderChildren([1, 6, 6, 3, 5])
  384. expect(`Duplicate keys`).toHaveBeenWarned()
  385. })
  386. })
  387. describe('renderer: unkeyed children', () => {
  388. let root: TestElement
  389. let elm: TestElement
  390. const renderChildren = (arr: Array<number | string>) => {
  391. render(h('div', arr.map(toSpan)), root)
  392. return root.children[0] as TestElement
  393. }
  394. beforeEach(() => {
  395. root = nodeOps.createElement('div')
  396. render(h('div', { id: 1 }, 'hello'), root)
  397. })
  398. test('move a key in non-keyed nodes with a size up', () => {
  399. elm = renderChildren([1, 'a', 'b', 'c'])
  400. expect(elm.children.length).toBe(4)
  401. expect((elm.children as TestElement[]).map(inner)).toEqual([
  402. '1',
  403. 'a',
  404. 'b',
  405. 'c',
  406. ])
  407. elm = renderChildren(['d', 'a', 'b', 'c', 1, 'e'])
  408. expect(elm.children.length).toBe(6)
  409. expect((elm.children as TestElement[]).map(inner)).toEqual([
  410. 'd',
  411. 'a',
  412. 'b',
  413. 'c',
  414. '1',
  415. 'e',
  416. ])
  417. })
  418. test('append elements with updating children without keys', () => {
  419. elm = renderChildren(['hello'])
  420. expect((elm.children as TestElement[]).map(inner)).toEqual(['hello'])
  421. elm = renderChildren(['hello', 'world'])
  422. expect((elm.children as TestElement[]).map(inner)).toEqual([
  423. 'hello',
  424. 'world',
  425. ])
  426. })
  427. test('unmoved text nodes with updating children without keys', () => {
  428. render(h('div', ['text', h('span', ['hello'])]), root)
  429. elm = root.children[0] as TestElement
  430. expect(elm.children[0]).toMatchObject({
  431. type: TestNodeTypes.TEXT,
  432. text: 'text',
  433. })
  434. render(h('div', ['text', h('span', ['hello'])]), root)
  435. elm = root.children[0] as TestElement
  436. expect(elm.children[0]).toMatchObject({
  437. type: TestNodeTypes.TEXT,
  438. text: 'text',
  439. })
  440. })
  441. test('changing text children with updating children without keys', () => {
  442. render(h('div', ['text', h('span', ['hello'])]), root)
  443. elm = root.children[0] as TestElement
  444. expect(elm.children[0]).toMatchObject({
  445. type: TestNodeTypes.TEXT,
  446. text: 'text',
  447. })
  448. render(h('div', ['text2', h('span', ['hello'])]), root)
  449. elm = root.children[0] as TestElement
  450. expect(elm.children[0]).toMatchObject({
  451. type: TestNodeTypes.TEXT,
  452. text: 'text2',
  453. })
  454. })
  455. test('prepend element with updating children without keys', () => {
  456. render(h('div', [h('span', ['world'])]), root)
  457. elm = root.children[0] as TestElement
  458. expect((elm.children as TestElement[]).map(inner)).toEqual(['world'])
  459. render(h('div', [h('span', ['hello']), h('span', ['world'])]), root)
  460. expect((elm.children as TestElement[]).map(inner)).toEqual([
  461. 'hello',
  462. 'world',
  463. ])
  464. })
  465. test('prepend element of different tag type with updating children without keys', () => {
  466. render(h('div', [h('span', ['world'])]), root)
  467. elm = root.children[0] as TestElement
  468. expect((elm.children as TestElement[]).map(inner)).toEqual(['world'])
  469. render(h('div', [h('div', ['hello']), h('span', ['world'])]), root)
  470. expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
  471. 'div',
  472. 'span',
  473. ])
  474. expect((elm.children as TestElement[]).map(inner)).toEqual([
  475. 'hello',
  476. 'world',
  477. ])
  478. })
  479. test('remove elements with updating children without keys', () => {
  480. render(
  481. h('div', [h('span', ['one']), h('span', ['two']), h('span', ['three'])]),
  482. root,
  483. )
  484. elm = root.children[0] as TestElement
  485. expect((elm.children as TestElement[]).map(inner)).toEqual([
  486. 'one',
  487. 'two',
  488. 'three',
  489. ])
  490. render(h('div', [h('span', ['one']), h('span', ['three'])]), root)
  491. elm = root.children[0] as TestElement
  492. expect((elm.children as TestElement[]).map(inner)).toEqual(['one', 'three'])
  493. })
  494. test('remove a single text node with updating children without keys', () => {
  495. render(h('div', ['one']), root)
  496. elm = root.children[0] as TestElement
  497. expect(serializeInner(elm)).toBe('one')
  498. render(h('div'), root)
  499. expect(serializeInner(elm)).toBe('')
  500. })
  501. test('remove a single text node when children are updated', () => {
  502. render(h('div', ['one']), root)
  503. elm = root.children[0] as TestElement
  504. expect(serializeInner(elm)).toBe('one')
  505. render(h('div', [h('div', ['two']), h('span', ['three'])]), root)
  506. elm = root.children[0] as TestElement
  507. expect((elm.children as TestElement[]).map(inner)).toEqual(['two', 'three'])
  508. })
  509. test('remove a text node among other elements', () => {
  510. render(h('div', ['one', h('span', ['two'])]), root)
  511. elm = root.children[0] as TestElement
  512. expect((elm.children as TestElement[]).map(c => serialize(c))).toEqual([
  513. 'one',
  514. '<span>two</span>',
  515. ])
  516. render(h('div', [h('div', ['three'])]), root)
  517. elm = root.children[0] as TestElement
  518. expect(elm.children.length).toBe(1)
  519. expect(serialize(elm.children[0])).toBe('<div>three</div>')
  520. })
  521. test('reorder elements', () => {
  522. render(
  523. h('div', [h('span', ['one']), h('div', ['two']), h('b', ['three'])]),
  524. root,
  525. )
  526. elm = root.children[0] as TestElement
  527. expect((elm.children as TestElement[]).map(inner)).toEqual([
  528. 'one',
  529. 'two',
  530. 'three',
  531. ])
  532. render(
  533. h('div', [h('b', ['three']), h('div', ['two']), h('span', ['one'])]),
  534. root,
  535. )
  536. elm = root.children[0] as TestElement
  537. expect((elm.children as TestElement[]).map(inner)).toEqual([
  538. 'three',
  539. 'two',
  540. 'one',
  541. ])
  542. })
  543. // #6502
  544. test('should not de-opt when both head and tail change', () => {
  545. render(h('div', [null, h('div'), null]), root)
  546. elm = root.children[0] as TestElement
  547. const original = elm.children[1]
  548. render(h('div', [h('p'), h('div'), h('p')]), root)
  549. elm = root.children[0] as TestElement
  550. const postPatch = elm.children[1]
  551. expect(postPatch).toBe(original)
  552. })
  553. })