rendererChildren.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  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. TestNodeTypes,
  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(
  273. h(
  274. 'span',
  275. arr.map(n => spanNumWithOpacity(n, '1'))
  276. ),
  277. root
  278. )
  279. elm = root.children[0] as TestElement
  280. for (let i = 0; i < elms; ++i) {
  281. expect(serializeInner(elm.children[i] as TestElement)).toBe(
  282. i.toString()
  283. )
  284. opacities[i] = Math.random().toFixed(5).toString()
  285. }
  286. const shufArr = shuffle(arr.slice(0))
  287. render(
  288. h(
  289. 'span',
  290. arr.map(n => spanNumWithOpacity(shufArr[n], opacities[n]))
  291. ),
  292. root
  293. )
  294. elm = root.children[0] as TestElement
  295. for (let i = 0; i < elms; ++i) {
  296. expect(serializeInner(elm.children[i] as TestElement)).toBe(
  297. shufArr[i].toString()
  298. )
  299. expect(elm.children[i]).toMatchObject({
  300. props: {
  301. style: {
  302. opacity: opacities[i]
  303. }
  304. }
  305. })
  306. }
  307. }
  308. })
  309. test('children with the same key but with different tag', () => {
  310. render(
  311. h('div', [
  312. h('div', { key: 1 }, 'one'),
  313. h('div', { key: 2 }, 'two'),
  314. h('div', { key: 3 }, 'three'),
  315. h('div', { key: 4 }, 'four')
  316. ]),
  317. root
  318. )
  319. elm = root.children[0] as TestElement
  320. expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
  321. 'div',
  322. 'div',
  323. 'div',
  324. 'div'
  325. ])
  326. expect((elm.children as TestElement[]).map(inner)).toEqual([
  327. 'one',
  328. 'two',
  329. 'three',
  330. 'four'
  331. ])
  332. render(
  333. h('div', [
  334. h('div', { key: 4 }, 'four'),
  335. h('span', { key: 3 }, 'three'),
  336. h('span', { key: 2 }, 'two'),
  337. h('div', { key: 1 }, 'one')
  338. ]),
  339. root
  340. )
  341. expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
  342. 'div',
  343. 'span',
  344. 'span',
  345. 'div'
  346. ])
  347. expect((elm.children as TestElement[]).map(inner)).toEqual([
  348. 'four',
  349. 'three',
  350. 'two',
  351. 'one'
  352. ])
  353. })
  354. test('children with the same tag, same key, but one with data and one without data', () => {
  355. render(h('div', [h('div', { class: 'hi' }, 'one')]), root)
  356. elm = root.children[0] as TestElement
  357. expect(elm.children[0]).toMatchObject({
  358. props: {
  359. class: 'hi'
  360. }
  361. })
  362. render(h('div', [h('div', 'four')]), root)
  363. elm = root.children[0] as TestElement
  364. expect(elm.children[0] as TestElement).toMatchObject({
  365. props: {
  366. // in the DOM renderer this will be ''
  367. // but the test renderer simply sets whatever value it receives.
  368. class: null
  369. }
  370. })
  371. expect(serialize(elm.children[0])).toBe(`<div>four</div>`)
  372. })
  373. test('should warn with duplicate keys', () => {
  374. renderChildren([1, 2, 3, 4, 5])
  375. renderChildren([1, 6, 6, 3, 5])
  376. expect(`Duplicate keys`).toHaveBeenWarned()
  377. })
  378. })
  379. describe('renderer: unkeyed children', () => {
  380. let root: TestElement
  381. let elm: TestElement
  382. const renderChildren = (arr: Array<number | string>) => {
  383. render(h('div', arr.map(toSpan)), root)
  384. return root.children[0] as TestElement
  385. }
  386. beforeEach(() => {
  387. root = nodeOps.createElement('div')
  388. render(h('div', { id: 1 }, 'hello'), root)
  389. })
  390. test('move a key in non-keyed nodes with a size up', () => {
  391. elm = renderChildren([1, 'a', 'b', 'c'])
  392. expect(elm.children.length).toBe(4)
  393. expect((elm.children as TestElement[]).map(inner)).toEqual([
  394. '1',
  395. 'a',
  396. 'b',
  397. 'c'
  398. ])
  399. elm = renderChildren(['d', 'a', 'b', 'c', 1, 'e'])
  400. expect(elm.children.length).toBe(6)
  401. expect((elm.children as TestElement[]).map(inner)).toEqual([
  402. 'd',
  403. 'a',
  404. 'b',
  405. 'c',
  406. '1',
  407. 'e'
  408. ])
  409. })
  410. test('append elements with updating children without keys', () => {
  411. elm = renderChildren(['hello'])
  412. expect((elm.children as TestElement[]).map(inner)).toEqual(['hello'])
  413. elm = renderChildren(['hello', 'world'])
  414. expect((elm.children as TestElement[]).map(inner)).toEqual([
  415. 'hello',
  416. 'world'
  417. ])
  418. })
  419. test('unmoved text nodes with updating children without keys', () => {
  420. render(h('div', ['text', h('span', ['hello'])]), root)
  421. elm = root.children[0] as TestElement
  422. expect(elm.children[0]).toMatchObject({
  423. type: TestNodeTypes.TEXT,
  424. text: 'text'
  425. })
  426. render(h('div', ['text', h('span', ['hello'])]), root)
  427. elm = root.children[0] as TestElement
  428. expect(elm.children[0]).toMatchObject({
  429. type: TestNodeTypes.TEXT,
  430. text: 'text'
  431. })
  432. })
  433. test('changing text children with updating children without keys', () => {
  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. render(h('div', ['text2', h('span', ['hello'])]), root)
  441. elm = root.children[0] as TestElement
  442. expect(elm.children[0]).toMatchObject({
  443. type: TestNodeTypes.TEXT,
  444. text: 'text2'
  445. })
  446. })
  447. test('prepend element with updating children without keys', () => {
  448. render(h('div', [h('span', ['world'])]), root)
  449. elm = root.children[0] as TestElement
  450. expect((elm.children as TestElement[]).map(inner)).toEqual(['world'])
  451. render(h('div', [h('span', ['hello']), h('span', ['world'])]), root)
  452. expect((elm.children as TestElement[]).map(inner)).toEqual([
  453. 'hello',
  454. 'world'
  455. ])
  456. })
  457. test('prepend element of different tag type with updating children without keys', () => {
  458. render(h('div', [h('span', ['world'])]), root)
  459. elm = root.children[0] as TestElement
  460. expect((elm.children as TestElement[]).map(inner)).toEqual(['world'])
  461. render(h('div', [h('div', ['hello']), h('span', ['world'])]), root)
  462. expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
  463. 'div',
  464. 'span'
  465. ])
  466. expect((elm.children as TestElement[]).map(inner)).toEqual([
  467. 'hello',
  468. 'world'
  469. ])
  470. })
  471. test('remove elements with updating children without keys', () => {
  472. render(
  473. h('div', [h('span', ['one']), h('span', ['two']), h('span', ['three'])]),
  474. root
  475. )
  476. elm = root.children[0] as TestElement
  477. expect((elm.children as TestElement[]).map(inner)).toEqual([
  478. 'one',
  479. 'two',
  480. 'three'
  481. ])
  482. render(h('div', [h('span', ['one']), h('span', ['three'])]), root)
  483. elm = root.children[0] as TestElement
  484. expect((elm.children as TestElement[]).map(inner)).toEqual(['one', 'three'])
  485. })
  486. test('remove a single text node with updating children without keys', () => {
  487. render(h('div', ['one']), root)
  488. elm = root.children[0] as TestElement
  489. expect(serializeInner(elm)).toBe('one')
  490. render(h('div'), root)
  491. expect(serializeInner(elm)).toBe('')
  492. })
  493. test('remove a single text node when children are updated', () => {
  494. render(h('div', ['one']), root)
  495. elm = root.children[0] as TestElement
  496. expect(serializeInner(elm)).toBe('one')
  497. render(h('div', [h('div', ['two']), h('span', ['three'])]), root)
  498. elm = root.children[0] as TestElement
  499. expect((elm.children as TestElement[]).map(inner)).toEqual(['two', 'three'])
  500. })
  501. test('remove a text node among other elements', () => {
  502. render(h('div', ['one', h('span', ['two'])]), root)
  503. elm = root.children[0] as TestElement
  504. expect((elm.children as TestElement[]).map(c => serialize(c))).toEqual([
  505. 'one',
  506. '<span>two</span>'
  507. ])
  508. render(h('div', [h('div', ['three'])]), root)
  509. elm = root.children[0] as TestElement
  510. expect(elm.children.length).toBe(1)
  511. expect(serialize(elm.children[0])).toBe('<div>three</div>')
  512. })
  513. test('reorder elements', () => {
  514. render(
  515. h('div', [h('span', ['one']), h('div', ['two']), h('b', ['three'])]),
  516. root
  517. )
  518. elm = root.children[0] as TestElement
  519. expect((elm.children as TestElement[]).map(inner)).toEqual([
  520. 'one',
  521. 'two',
  522. 'three'
  523. ])
  524. render(
  525. h('div', [h('b', ['three']), h('div', ['two']), h('span', ['one'])]),
  526. root
  527. )
  528. elm = root.children[0] as TestElement
  529. expect((elm.children as TestElement[]).map(inner)).toEqual([
  530. 'three',
  531. 'two',
  532. 'one'
  533. ])
  534. })
  535. // #6502
  536. test('should not de-opt when both head and tail change', () => {
  537. render(h('div', [null, h('div'), null]), root)
  538. elm = root.children[0] as TestElement
  539. const original = elm.children[1]
  540. render(h('div', [h('p'), h('div'), h('p')]), root)
  541. elm = root.children[0] as TestElement
  542. const postPatch = elm.children[1]
  543. expect(postPatch).toBe(original)
  544. })
  545. })