rendererChildren.spec.ts 16 KB

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