rendererChildren.spec.ts 16 KB

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