TransitionGroup.spec.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. // @vitest-environment jsdom
  2. import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
  3. import path from 'node:path'
  4. import { createApp, ref } from 'vue'
  5. describe('e2e: TransitionGroup', () => {
  6. const { page, html, nextFrame, timeout } = setupPuppeteer()
  7. const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
  8. const duration = process.env.CI ? 200 : 50
  9. const buffer = process.env.CI ? 20 : 5
  10. const htmlWhenTransitionStart = () =>
  11. page().evaluate(() => {
  12. ;(document.querySelector('#toggleBtn') as any)!.click()
  13. return Promise.resolve().then(() => {
  14. return document.querySelector('#container')!.innerHTML
  15. })
  16. })
  17. const transitionFinish = (time = duration) => timeout(time + buffer)
  18. beforeEach(async () => {
  19. await page().goto(baseUrl)
  20. await page().waitForSelector('#app')
  21. })
  22. test(
  23. 'enter',
  24. async () => {
  25. await page().evaluate(() => {
  26. const { createApp, ref } = (window as any).Vue
  27. createApp({
  28. template: `
  29. <div id="container">
  30. <transition-group name="test">
  31. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  32. </transition-group>
  33. </div>
  34. <button id="toggleBtn" @click="click">button</button>
  35. `,
  36. setup: () => {
  37. const items = ref(['a', 'b', 'c'])
  38. const click = () => items.value.push('d', 'e')
  39. return { click, items }
  40. },
  41. }).mount('#app')
  42. })
  43. expect(await html('#container')).toBe(
  44. `<div class="test">a</div>` +
  45. `<div class="test">b</div>` +
  46. `<div class="test">c</div>`,
  47. )
  48. expect(await htmlWhenTransitionStart()).toBe(
  49. `<div class="test">a</div>` +
  50. `<div class="test">b</div>` +
  51. `<div class="test">c</div>` +
  52. `<div class="test test-enter-from test-enter-active">d</div>` +
  53. `<div class="test test-enter-from test-enter-active">e</div>`,
  54. )
  55. await nextFrame()
  56. expect(await html('#container')).toBe(
  57. `<div class="test">a</div>` +
  58. `<div class="test">b</div>` +
  59. `<div class="test">c</div>` +
  60. `<div class="test test-enter-active test-enter-to">d</div>` +
  61. `<div class="test test-enter-active test-enter-to">e</div>`,
  62. )
  63. await transitionFinish()
  64. expect(await html('#container')).toBe(
  65. `<div class="test">a</div>` +
  66. `<div class="test">b</div>` +
  67. `<div class="test">c</div>` +
  68. `<div class="test">d</div>` +
  69. `<div class="test">e</div>`,
  70. )
  71. },
  72. E2E_TIMEOUT,
  73. )
  74. test(
  75. 'leave',
  76. async () => {
  77. await page().evaluate(() => {
  78. const { createApp, ref } = (window as any).Vue
  79. createApp({
  80. template: `
  81. <div id="container">
  82. <transition-group name="test">
  83. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  84. </transition-group>
  85. </div>
  86. <button id="toggleBtn" @click="click">button</button>
  87. `,
  88. setup: () => {
  89. const items = ref(['a', 'b', 'c'])
  90. const click = () => (items.value = ['b'])
  91. return { click, items }
  92. },
  93. }).mount('#app')
  94. })
  95. expect(await html('#container')).toBe(
  96. `<div class="test">a</div>` +
  97. `<div class="test">b</div>` +
  98. `<div class="test">c</div>`,
  99. )
  100. expect(await htmlWhenTransitionStart()).toBe(
  101. `<div class="test test-leave-from test-leave-active">a</div>` +
  102. `<div class="test">b</div>` +
  103. `<div class="test test-leave-from test-leave-active">c</div>`,
  104. )
  105. await nextFrame()
  106. expect(await html('#container')).toBe(
  107. `<div class="test test-leave-active test-leave-to">a</div>` +
  108. `<div class="test">b</div>` +
  109. `<div class="test test-leave-active test-leave-to">c</div>`,
  110. )
  111. await transitionFinish()
  112. expect(await html('#container')).toBe(`<div class="test">b</div>`)
  113. },
  114. E2E_TIMEOUT,
  115. )
  116. test(
  117. 'enter + leave',
  118. async () => {
  119. await page().evaluate(() => {
  120. const { createApp, ref } = (window as any).Vue
  121. createApp({
  122. template: `
  123. <div id="container">
  124. <transition-group name="test">
  125. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  126. </transition-group>
  127. </div>
  128. <button id="toggleBtn" @click="click">button</button>
  129. `,
  130. setup: () => {
  131. const items = ref(['a', 'b', 'c'])
  132. const click = () => (items.value = ['b', 'c', 'd'])
  133. return { click, items }
  134. },
  135. }).mount('#app')
  136. })
  137. expect(await html('#container')).toBe(
  138. `<div class="test">a</div>` +
  139. `<div class="test">b</div>` +
  140. `<div class="test">c</div>`,
  141. )
  142. expect(await htmlWhenTransitionStart()).toBe(
  143. `<div class="test test-leave-from test-leave-active">a</div>` +
  144. `<div class="test">b</div>` +
  145. `<div class="test">c</div>` +
  146. `<div class="test test-enter-from test-enter-active">d</div>`,
  147. )
  148. await nextFrame()
  149. expect(await html('#container')).toBe(
  150. `<div class="test test-leave-active test-leave-to">a</div>` +
  151. `<div class="test">b</div>` +
  152. `<div class="test">c</div>` +
  153. `<div class="test test-enter-active test-enter-to">d</div>`,
  154. )
  155. await transitionFinish()
  156. expect(await html('#container')).toBe(
  157. `<div class="test">b</div>` +
  158. `<div class="test">c</div>` +
  159. `<div class="test">d</div>`,
  160. )
  161. },
  162. E2E_TIMEOUT,
  163. )
  164. test(
  165. 'appear',
  166. async () => {
  167. const appearHtml = await page().evaluate(() => {
  168. const { createApp, ref } = (window as any).Vue
  169. createApp({
  170. template: `
  171. <div id="container">
  172. <transition-group appear
  173. appear-from-class="test-appear-from"
  174. appear-to-class="test-appear-to"
  175. appear-active-class="test-appear-active"
  176. name="test">
  177. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  178. </transition-group>
  179. </div>
  180. <button id="toggleBtn" @click="click">button</button>
  181. `,
  182. setup: () => {
  183. const items = ref(['a', 'b', 'c'])
  184. const click = () => items.value.push('d', 'e')
  185. return { click, items }
  186. },
  187. }).mount('#app')
  188. return Promise.resolve().then(() => {
  189. return document.querySelector('#container')!.innerHTML
  190. })
  191. })
  192. // appear
  193. expect(appearHtml).toBe(
  194. `<div class="test test-appear-from test-appear-active">a</div>` +
  195. `<div class="test test-appear-from test-appear-active">b</div>` +
  196. `<div class="test test-appear-from test-appear-active">c</div>`,
  197. )
  198. await nextFrame()
  199. expect(await html('#container')).toBe(
  200. `<div class="test test-appear-active test-appear-to">a</div>` +
  201. `<div class="test test-appear-active test-appear-to">b</div>` +
  202. `<div class="test test-appear-active test-appear-to">c</div>`,
  203. )
  204. await transitionFinish()
  205. expect(await html('#container')).toBe(
  206. `<div class="test">a</div>` +
  207. `<div class="test">b</div>` +
  208. `<div class="test">c</div>`,
  209. )
  210. // enter
  211. expect(await htmlWhenTransitionStart()).toBe(
  212. `<div class="test">a</div>` +
  213. `<div class="test">b</div>` +
  214. `<div class="test">c</div>` +
  215. `<div class="test test-enter-from test-enter-active">d</div>` +
  216. `<div class="test test-enter-from test-enter-active">e</div>`,
  217. )
  218. await nextFrame()
  219. expect(await html('#container')).toBe(
  220. `<div class="test">a</div>` +
  221. `<div class="test">b</div>` +
  222. `<div class="test">c</div>` +
  223. `<div class="test test-enter-active test-enter-to">d</div>` +
  224. `<div class="test test-enter-active test-enter-to">e</div>`,
  225. )
  226. await transitionFinish()
  227. expect(await html('#container')).toBe(
  228. `<div class="test">a</div>` +
  229. `<div class="test">b</div>` +
  230. `<div class="test">c</div>` +
  231. `<div class="test">d</div>` +
  232. `<div class="test">e</div>`,
  233. )
  234. },
  235. E2E_TIMEOUT,
  236. )
  237. test(
  238. 'move',
  239. async () => {
  240. await page().evaluate(() => {
  241. const { createApp, ref } = (window as any).Vue
  242. createApp({
  243. template: `
  244. <div id="container">
  245. <transition-group name="group">
  246. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  247. </transition-group>
  248. </div>
  249. <button id="toggleBtn" @click="click">button</button>
  250. `,
  251. setup: () => {
  252. const items = ref(['a', 'b', 'c'])
  253. const click = () => (items.value = ['d', 'b', 'a'])
  254. return { click, items }
  255. },
  256. }).mount('#app')
  257. })
  258. expect(await html('#container')).toBe(
  259. `<div class="test">a</div>` +
  260. `<div class="test">b</div>` +
  261. `<div class="test">c</div>`,
  262. )
  263. expect(await htmlWhenTransitionStart()).toBe(
  264. `<div class="test group-enter-from group-enter-active">d</div>` +
  265. `<div class="test">b</div>` +
  266. `<div class="test group-move" style="">a</div>` +
  267. `<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
  268. )
  269. await nextFrame()
  270. expect(await html('#container')).toBe(
  271. `<div class="test group-enter-active group-enter-to">d</div>` +
  272. `<div class="test">b</div>` +
  273. `<div class="test group-move" style="">a</div>` +
  274. `<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
  275. )
  276. await transitionFinish(duration * 2)
  277. expect(await html('#container')).toBe(
  278. `<div class="test">d</div>` +
  279. `<div class="test">b</div>` +
  280. `<div class="test" style="">a</div>`,
  281. )
  282. },
  283. E2E_TIMEOUT,
  284. )
  285. test(
  286. 'move while entering',
  287. async () => {
  288. await page().evaluate(duration => {
  289. const { createApp, ref, onMounted } = (window as any).Vue
  290. createApp({
  291. template: `
  292. <transition-group name="toasts" tag="div" id="toasts">
  293. <div class="toast" v-for="toast in list" :key="toast.id">
  294. {{ toast.text }} #{{ toast.id }}
  295. </div>
  296. </transition-group>
  297. <button id="addBtn" @click="add">button</button>
  298. `,
  299. setup: () => {
  300. const list = ref([])
  301. let id = 0
  302. const add = () => {
  303. if (list.value.length > 3) {
  304. list.value.splice(0, 1)
  305. }
  306. list.value.push({
  307. id,
  308. type: 'error',
  309. text: 'Test message',
  310. })
  311. id++
  312. }
  313. onMounted(() => {
  314. const styleNode = document.createElement('style')
  315. styleNode.innerHTML = `
  316. #toasts {
  317. position: absolute;
  318. bottom: 0;
  319. left: 0;
  320. }
  321. #toasts > .toast {
  322. width: 150px;
  323. margin-bottom: 10px;
  324. height: 30px;
  325. color: white;
  326. background: black;
  327. }
  328. .toasts-leave-active {
  329. position: absolute;
  330. }
  331. .toasts-move { transition: transform ${duration}ms ease; }
  332. `
  333. document.body.appendChild(styleNode)
  334. })
  335. return { list, add }
  336. },
  337. }).mount('#app')
  338. }, duration)
  339. const overlapDelay = Math.max(10, Math.floor(duration / 2))
  340. const { midTop, finalTop } = await page().evaluate(
  341. ({ overlapDelay, duration, buffer }) => {
  342. ;(document.querySelector('#addBtn') as any)!.click()
  343. return new Promise<{ midTop: number; finalTop: number }>(resolve => {
  344. setTimeout(() => {
  345. ;(document.querySelector('#addBtn') as any)!.click()
  346. Promise.resolve().then(() => {
  347. const nodes = Array.from(
  348. document.querySelectorAll('#toasts .toast'),
  349. ) as HTMLElement[]
  350. const firstToast = nodes.find(node =>
  351. node.textContent?.includes('#0'),
  352. )
  353. const midTop = firstToast
  354. ? firstToast.getBoundingClientRect().top
  355. : NaN
  356. setTimeout(() => {
  357. const finalTop = firstToast
  358. ? firstToast.getBoundingClientRect().top
  359. : NaN
  360. resolve({ midTop, finalTop })
  361. }, duration + buffer)
  362. })
  363. }, overlapDelay)
  364. })
  365. },
  366. { overlapDelay, duration, buffer },
  367. )
  368. expect(midTop).toBeGreaterThan(finalTop)
  369. },
  370. E2E_TIMEOUT,
  371. )
  372. test(
  373. 'dynamic name',
  374. async () => {
  375. await page().evaluate(() => {
  376. const { createApp, ref } = (window as any).Vue
  377. createApp({
  378. template: `
  379. <div id="container">
  380. <transition-group :name="name">
  381. <div v-for="item in items" :key="item" >{{item}}</div>
  382. </transition-group>
  383. </div>
  384. <button id="toggleBtn" @click="click">button</button>
  385. <button id="changeNameBtn" @click="changeName">button</button>
  386. `,
  387. setup: () => {
  388. const items = ref(['a', 'b', 'c'])
  389. const name = ref('invalid')
  390. const click = () => (items.value = ['b', 'c', 'a'])
  391. const changeName = () => {
  392. name.value = 'group'
  393. items.value = ['a', 'b', 'c']
  394. }
  395. return { click, items, name, changeName }
  396. },
  397. }).mount('#app')
  398. })
  399. expect(await html('#container')).toBe(
  400. `<div>a</div>` + `<div>b</div>` + `<div>c</div>`,
  401. )
  402. // invalid name
  403. expect(await htmlWhenTransitionStart()).toBe(
  404. `<div>b</div>` + `<div>c</div>` + `<div>a</div>`,
  405. )
  406. // change name
  407. const moveHtml = await page().evaluate(() => {
  408. ;(document.querySelector('#changeNameBtn') as any).click()
  409. return Promise.resolve().then(() => {
  410. return document.querySelector('#container')!.innerHTML
  411. })
  412. })
  413. expect(moveHtml).toBe(
  414. `<div class="group-move" style="">a</div>` +
  415. `<div class="group-move" style="">b</div>` +
  416. `<div class="group-move" style="">c</div>`,
  417. )
  418. // not sure why but we just have to wait really long for this to
  419. // pass consistently :/
  420. await transitionFinish(duration * 4 + buffer)
  421. expect(await html('#container')).toBe(
  422. `<div class="" style="">a</div>` +
  423. `<div class="" style="">b</div>` +
  424. `<div class="" style="">c</div>`,
  425. )
  426. },
  427. E2E_TIMEOUT,
  428. )
  429. test(
  430. 'events',
  431. async () => {
  432. const onLeaveSpy = vi.fn()
  433. const onEnterSpy = vi.fn()
  434. const onAppearSpy = vi.fn()
  435. const beforeLeaveSpy = vi.fn()
  436. const beforeEnterSpy = vi.fn()
  437. const beforeAppearSpy = vi.fn()
  438. const afterLeaveSpy = vi.fn()
  439. const afterEnterSpy = vi.fn()
  440. const afterAppearSpy = vi.fn()
  441. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  442. await page().exposeFunction('onEnterSpy', onEnterSpy)
  443. await page().exposeFunction('onAppearSpy', onAppearSpy)
  444. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  445. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  446. await page().exposeFunction('beforeAppearSpy', beforeAppearSpy)
  447. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  448. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  449. await page().exposeFunction('afterAppearSpy', afterAppearSpy)
  450. const appearHtml = await page().evaluate(() => {
  451. const {
  452. beforeAppearSpy,
  453. onAppearSpy,
  454. afterAppearSpy,
  455. beforeEnterSpy,
  456. onEnterSpy,
  457. afterEnterSpy,
  458. beforeLeaveSpy,
  459. onLeaveSpy,
  460. afterLeaveSpy,
  461. } = window as any
  462. const { createApp, ref } = (window as any).Vue
  463. createApp({
  464. template: `
  465. <div id="container">
  466. <transition-group name="test"
  467. appear
  468. appear-from-class="test-appear-from"
  469. appear-to-class="test-appear-to"
  470. appear-active-class="test-appear-active"
  471. @before-enter="beforeEnterSpy()"
  472. @enter="onEnterSpy()"
  473. @after-enter="afterEnterSpy()"
  474. @before-leave="beforeLeaveSpy()"
  475. @leave="onLeaveSpy()"
  476. @after-leave="afterLeaveSpy()"
  477. @before-appear="beforeAppearSpy()"
  478. @appear="onAppearSpy()"
  479. @after-appear="afterAppearSpy()">
  480. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  481. </transition-group>
  482. </div>
  483. <button id="toggleBtn" @click="click">button</button>
  484. `,
  485. setup: () => {
  486. const items = ref(['a', 'b', 'c'])
  487. const click = () => (items.value = ['b', 'c', 'd'])
  488. return {
  489. click,
  490. items,
  491. beforeAppearSpy,
  492. onAppearSpy,
  493. afterAppearSpy,
  494. beforeEnterSpy,
  495. onEnterSpy,
  496. afterEnterSpy,
  497. beforeLeaveSpy,
  498. onLeaveSpy,
  499. afterLeaveSpy,
  500. }
  501. },
  502. }).mount('#app')
  503. return Promise.resolve().then(() => {
  504. return document.querySelector('#container')!.innerHTML
  505. })
  506. })
  507. expect(beforeAppearSpy).toBeCalled()
  508. expect(onAppearSpy).toBeCalled()
  509. expect(afterAppearSpy).not.toBeCalled()
  510. expect(appearHtml).toBe(
  511. `<div class="test test-appear-from test-appear-active">a</div>` +
  512. `<div class="test test-appear-from test-appear-active">b</div>` +
  513. `<div class="test test-appear-from test-appear-active">c</div>`,
  514. )
  515. await nextFrame()
  516. expect(afterAppearSpy).not.toBeCalled()
  517. expect(await html('#container')).toBe(
  518. `<div class="test test-appear-active test-appear-to">a</div>` +
  519. `<div class="test test-appear-active test-appear-to">b</div>` +
  520. `<div class="test test-appear-active test-appear-to">c</div>`,
  521. )
  522. await transitionFinish()
  523. expect(afterAppearSpy).toBeCalled()
  524. expect(await html('#container')).toBe(
  525. `<div class="test">a</div>` +
  526. `<div class="test">b</div>` +
  527. `<div class="test">c</div>`,
  528. )
  529. // enter + leave
  530. expect(await htmlWhenTransitionStart()).toBe(
  531. `<div class="test test-leave-from test-leave-active">a</div>` +
  532. `<div class="test">b</div>` +
  533. `<div class="test">c</div>` +
  534. `<div class="test test-enter-from test-enter-active">d</div>`,
  535. )
  536. expect(beforeLeaveSpy).toBeCalled()
  537. expect(onLeaveSpy).toBeCalled()
  538. expect(afterLeaveSpy).not.toBeCalled()
  539. expect(beforeEnterSpy).toBeCalled()
  540. expect(onEnterSpy).toBeCalled()
  541. expect(afterEnterSpy).not.toBeCalled()
  542. await nextFrame()
  543. expect(await html('#container')).toBe(
  544. `<div class="test test-leave-active test-leave-to">a</div>` +
  545. `<div class="test">b</div>` +
  546. `<div class="test">c</div>` +
  547. `<div class="test test-enter-active test-enter-to">d</div>`,
  548. )
  549. expect(afterLeaveSpy).not.toBeCalled()
  550. expect(afterEnterSpy).not.toBeCalled()
  551. await transitionFinish()
  552. expect(await html('#container')).toBe(
  553. `<div class="test">b</div>` +
  554. `<div class="test">c</div>` +
  555. `<div class="test">d</div>`,
  556. )
  557. expect(afterLeaveSpy).toBeCalled()
  558. expect(afterEnterSpy).toBeCalled()
  559. },
  560. E2E_TIMEOUT,
  561. )
  562. test('warn unkeyed children', () => {
  563. createApp({
  564. template: `
  565. <transition-group name="test">
  566. <div v-for="item in items" class="test">{{item}}</div>
  567. </transition-group>
  568. `,
  569. setup: () => {
  570. const items = ref(['a', 'b', 'c'])
  571. return { items }
  572. },
  573. }).mount(document.createElement('div'))
  574. expect(`<TransitionGroup> children must be keyed`).toHaveBeenWarned()
  575. })
  576. test('not warn unkeyed text children w/ whitespace preserve', () => {
  577. const app = createApp({
  578. template: `
  579. <transition-group name="test">
  580. <p key="1">1</p>
  581. <p key="2" v-if="false">2</p>
  582. </transition-group>
  583. `,
  584. })
  585. app.config.compilerOptions.whitespace = 'preserve'
  586. app.mount(document.createElement('div'))
  587. expect(`<TransitionGroup> children must be keyed`).not.toHaveBeenWarned()
  588. })
  589. // #5168, #7898, #9067
  590. test(
  591. 'avoid set transition hooks for comment node',
  592. async () => {
  593. await page().evaluate(duration => {
  594. const { createApp, ref, h, createCommentVNode } = (window as any).Vue
  595. const show = ref(false)
  596. createApp({
  597. template: `
  598. <div id="container">
  599. <transition-group name="test">
  600. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  601. <Child key="child"/>
  602. </transition-group>
  603. </div>
  604. <button id="toggleBtn" @click="click">button</button>
  605. `,
  606. components: {
  607. Child: {
  608. setup() {
  609. return () =>
  610. show.value
  611. ? h('div', { class: 'test' }, 'child')
  612. : createCommentVNode('v-if', true)
  613. },
  614. },
  615. },
  616. setup: () => {
  617. const items = ref([])
  618. const click = () => {
  619. items.value = ['a', 'b', 'c']
  620. setTimeout(() => {
  621. show.value = true
  622. }, duration)
  623. }
  624. return { click, items }
  625. },
  626. }).mount('#app')
  627. }, duration)
  628. expect(await html('#container')).toBe(`<!--v-if-->`)
  629. expect(await htmlWhenTransitionStart()).toBe(
  630. `<div class="test test-enter-from test-enter-active">a</div>` +
  631. `<div class="test test-enter-from test-enter-active">b</div>` +
  632. `<div class="test test-enter-from test-enter-active">c</div>` +
  633. `<!--v-if-->`,
  634. )
  635. await transitionFinish(duration)
  636. await nextFrame()
  637. expect(await html('#container')).toBe(
  638. `<div class="test">a</div>` +
  639. `<div class="test">b</div>` +
  640. `<div class="test">c</div>` +
  641. `<div class="test test-enter-active test-enter-to">child</div>`,
  642. )
  643. await transitionFinish(duration)
  644. expect(await html('#container')).toBe(
  645. `<div class="test">a</div>` +
  646. `<div class="test">b</div>` +
  647. `<div class="test">c</div>` +
  648. `<div class="test">child</div>`,
  649. )
  650. },
  651. E2E_TIMEOUT,
  652. )
  653. // #4621, #4622, #5153
  654. test(
  655. 'avoid set transition hooks for text node',
  656. async () => {
  657. await page().evaluate(() => {
  658. const { createApp, ref } = (window as any).Vue
  659. const app = createApp({
  660. template: `
  661. <div id="container">
  662. <transition-group name="test">
  663. <div class="test">foo</div>
  664. <div class="test" v-if="show">bar</div>
  665. </transition-group>
  666. </div>
  667. <button id="toggleBtn" @click="click">button</button>
  668. `,
  669. setup: () => {
  670. const show = ref(false)
  671. const click = () => {
  672. show.value = true
  673. }
  674. return { show, click }
  675. },
  676. })
  677. app.config.compilerOptions.whitespace = 'preserve'
  678. app.mount('#app')
  679. })
  680. expect(await html('#container')).toBe(`<div class="test">foo</div>` + ` `)
  681. expect(await htmlWhenTransitionStart()).toBe(
  682. `<div class="test">foo</div>` +
  683. ` ` +
  684. `<div class="test test-enter-from test-enter-active">bar</div>`,
  685. )
  686. await nextFrame()
  687. expect(await html('#container')).toBe(
  688. `<div class="test">foo</div>` +
  689. ` ` +
  690. `<div class="test test-enter-active test-enter-to">bar</div>`,
  691. )
  692. await transitionFinish(duration)
  693. expect(await html('#container')).toBe(
  694. `<div class="test">foo</div>` + ` ` + `<div class="test">bar</div>`,
  695. )
  696. },
  697. E2E_TIMEOUT,
  698. )
  699. // #6105
  700. test(
  701. 'with scale',
  702. async () => {
  703. await page().evaluate(() => {
  704. const { createApp, ref, onMounted } = (window as any).Vue
  705. createApp({
  706. template: `
  707. <div id="container">
  708. <div class="scale" style="transform: scale(2) translateX(50%) translateY(50%)">
  709. <transition-group tag="ul">
  710. <li v-for="item in items" :key="item">{{item}}</li>
  711. </transition-group>
  712. <button id="toggleBtn" @click="click">button</button>
  713. </div>
  714. </div>
  715. `,
  716. setup: () => {
  717. const items = ref(['a', 'b', 'c'])
  718. const click = () => {
  719. items.value.reverse()
  720. }
  721. onMounted(() => {
  722. const styleNode = document.createElement('style')
  723. styleNode.innerHTML = `.v-move {
  724. transition: transform 0.5s ease;
  725. }`
  726. document.body.appendChild(styleNode)
  727. })
  728. return { items, click }
  729. },
  730. }).mount('#app')
  731. })
  732. const original_top = await page().$eval('ul li:nth-child(1)', node => {
  733. return node.getBoundingClientRect().top
  734. })
  735. const new_top = await page().evaluate(() => {
  736. const el = document.querySelector('ul li:nth-child(1)')
  737. const p = new Promise(resolve => {
  738. el!.addEventListener('transitionstart', () => {
  739. const new_top = el!.getBoundingClientRect().top
  740. resolve(new_top)
  741. })
  742. })
  743. ;(document.querySelector('#toggleBtn') as any)!.click()
  744. return p
  745. })
  746. expect(original_top).toBeLessThan(new_top as number)
  747. },
  748. E2E_TIMEOUT,
  749. )
  750. test(
  751. 'not leaking after children unmounted',
  752. async () => {
  753. const client = await page().createCDPSession()
  754. await page().evaluate(async () => {
  755. const { createApp, ref, nextTick } = (window as any).Vue
  756. const show = ref(true)
  757. createApp({
  758. components: {
  759. Child: {
  760. setup: () => {
  761. // Big arrays kick GC earlier
  762. const test = ref([...Array(3000)].map((_, i) => ({ i })))
  763. // @ts-expect-error - Custom property and same lib as runtime is used
  764. window.__REF__ = new WeakRef(test)
  765. return { test }
  766. },
  767. template: `
  768. <p>{{ test.length }}</p>
  769. `,
  770. },
  771. },
  772. template: `
  773. <transition-group>
  774. <Child v-if="show" />
  775. </transition-group>
  776. `,
  777. setup() {
  778. return { show }
  779. },
  780. }).mount('#app')
  781. show.value = false
  782. await nextTick()
  783. })
  784. const isCollected = async () =>
  785. // @ts-expect-error - Custom property
  786. await page().evaluate(() => window.__REF__.deref() === undefined)
  787. while ((await isCollected()) === false) {
  788. await client.send('HeapProfiler.collectGarbage')
  789. }
  790. expect(await isCollected()).toBe(true)
  791. },
  792. E2E_TIMEOUT,
  793. )
  794. })