TransitionGroup.spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
  2. import path from 'node:path'
  3. import { createApp, ref } from 'vue'
  4. describe('e2e: TransitionGroup', () => {
  5. const { page, html, nextFrame, timeout } = setupPuppeteer()
  6. const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
  7. const duration = process.env.CI ? 200 : 50
  8. const buffer = process.env.CI ? 20 : 5
  9. const htmlWhenTransitionStart = () =>
  10. page().evaluate(() => {
  11. ;(document.querySelector('#toggleBtn') as any)!.click()
  12. return Promise.resolve().then(() => {
  13. return document.querySelector('#container')!.innerHTML
  14. })
  15. })
  16. const transitionFinish = (time = duration) => timeout(time + buffer)
  17. beforeEach(async () => {
  18. await page().goto(baseUrl)
  19. await page().waitForSelector('#app')
  20. })
  21. test(
  22. 'enter',
  23. async () => {
  24. await page().evaluate(() => {
  25. const { createApp, ref } = (window as any).Vue
  26. createApp({
  27. template: `
  28. <div id="container">
  29. <transition-group name="test">
  30. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  31. </transition-group>
  32. </div>
  33. <button id="toggleBtn" @click="click">button</button>
  34. `,
  35. setup: () => {
  36. const items = ref(['a', 'b', 'c'])
  37. const click = () => items.value.push('d', 'e')
  38. return { click, items }
  39. },
  40. }).mount('#app')
  41. })
  42. expect(await html('#container')).toBe(
  43. `<div class="test">a</div>` +
  44. `<div class="test">b</div>` +
  45. `<div class="test">c</div>`,
  46. )
  47. expect(await htmlWhenTransitionStart()).toBe(
  48. `<div class="test">a</div>` +
  49. `<div class="test">b</div>` +
  50. `<div class="test">c</div>` +
  51. `<div class="test test-enter-from test-enter-active">d</div>` +
  52. `<div class="test test-enter-from test-enter-active">e</div>`,
  53. )
  54. await nextFrame()
  55. expect(await html('#container')).toBe(
  56. `<div class="test">a</div>` +
  57. `<div class="test">b</div>` +
  58. `<div class="test">c</div>` +
  59. `<div class="test test-enter-active test-enter-to">d</div>` +
  60. `<div class="test test-enter-active test-enter-to">e</div>`,
  61. )
  62. await transitionFinish()
  63. expect(await html('#container')).toBe(
  64. `<div class="test">a</div>` +
  65. `<div class="test">b</div>` +
  66. `<div class="test">c</div>` +
  67. `<div class="test">d</div>` +
  68. `<div class="test">e</div>`,
  69. )
  70. },
  71. E2E_TIMEOUT,
  72. )
  73. test(
  74. 'leave',
  75. async () => {
  76. await page().evaluate(() => {
  77. const { createApp, ref } = (window as any).Vue
  78. createApp({
  79. template: `
  80. <div id="container">
  81. <transition-group name="test">
  82. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  83. </transition-group>
  84. </div>
  85. <button id="toggleBtn" @click="click">button</button>
  86. `,
  87. setup: () => {
  88. const items = ref(['a', 'b', 'c'])
  89. const click = () => (items.value = ['b'])
  90. return { click, items }
  91. },
  92. }).mount('#app')
  93. })
  94. expect(await html('#container')).toBe(
  95. `<div class="test">a</div>` +
  96. `<div class="test">b</div>` +
  97. `<div class="test">c</div>`,
  98. )
  99. expect(await htmlWhenTransitionStart()).toBe(
  100. `<div class="test test-leave-from test-leave-active">a</div>` +
  101. `<div class="test">b</div>` +
  102. `<div class="test test-leave-from test-leave-active">c</div>`,
  103. )
  104. await nextFrame()
  105. expect(await html('#container')).toBe(
  106. `<div class="test test-leave-active test-leave-to">a</div>` +
  107. `<div class="test">b</div>` +
  108. `<div class="test test-leave-active test-leave-to">c</div>`,
  109. )
  110. await transitionFinish()
  111. expect(await html('#container')).toBe(`<div class="test">b</div>`)
  112. },
  113. E2E_TIMEOUT,
  114. )
  115. test(
  116. 'enter + leave',
  117. async () => {
  118. await page().evaluate(() => {
  119. const { createApp, ref } = (window as any).Vue
  120. createApp({
  121. template: `
  122. <div id="container">
  123. <transition-group name="test">
  124. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  125. </transition-group>
  126. </div>
  127. <button id="toggleBtn" @click="click">button</button>
  128. `,
  129. setup: () => {
  130. const items = ref(['a', 'b', 'c'])
  131. const click = () => (items.value = ['b', 'c', 'd'])
  132. return { click, items }
  133. },
  134. }).mount('#app')
  135. })
  136. expect(await html('#container')).toBe(
  137. `<div class="test">a</div>` +
  138. `<div class="test">b</div>` +
  139. `<div class="test">c</div>`,
  140. )
  141. expect(await htmlWhenTransitionStart()).toBe(
  142. `<div class="test test-leave-from test-leave-active">a</div>` +
  143. `<div class="test">b</div>` +
  144. `<div class="test">c</div>` +
  145. `<div class="test test-enter-from test-enter-active">d</div>`,
  146. )
  147. await nextFrame()
  148. expect(await html('#container')).toBe(
  149. `<div class="test test-leave-active test-leave-to">a</div>` +
  150. `<div class="test">b</div>` +
  151. `<div class="test">c</div>` +
  152. `<div class="test test-enter-active test-enter-to">d</div>`,
  153. )
  154. await transitionFinish()
  155. expect(await html('#container')).toBe(
  156. `<div class="test">b</div>` +
  157. `<div class="test">c</div>` +
  158. `<div class="test">d</div>`,
  159. )
  160. },
  161. E2E_TIMEOUT,
  162. )
  163. test(
  164. 'appear',
  165. async () => {
  166. const appearHtml = await page().evaluate(() => {
  167. const { createApp, ref } = (window as any).Vue
  168. createApp({
  169. template: `
  170. <div id="container">
  171. <transition-group appear
  172. appear-from-class="test-appear-from"
  173. appear-to-class="test-appear-to"
  174. appear-active-class="test-appear-active"
  175. name="test">
  176. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  177. </transition-group>
  178. </div>
  179. <button id="toggleBtn" @click="click">button</button>
  180. `,
  181. setup: () => {
  182. const items = ref(['a', 'b', 'c'])
  183. const click = () => items.value.push('d', 'e')
  184. return { click, items }
  185. },
  186. }).mount('#app')
  187. return Promise.resolve().then(() => {
  188. return document.querySelector('#container')!.innerHTML
  189. })
  190. })
  191. // appear
  192. expect(appearHtml).toBe(
  193. `<div class="test test-appear-from test-appear-active">a</div>` +
  194. `<div class="test test-appear-from test-appear-active">b</div>` +
  195. `<div class="test test-appear-from test-appear-active">c</div>`,
  196. )
  197. await nextFrame()
  198. expect(await html('#container')).toBe(
  199. `<div class="test test-appear-active test-appear-to">a</div>` +
  200. `<div class="test test-appear-active test-appear-to">b</div>` +
  201. `<div class="test test-appear-active test-appear-to">c</div>`,
  202. )
  203. await transitionFinish()
  204. expect(await html('#container')).toBe(
  205. `<div class="test">a</div>` +
  206. `<div class="test">b</div>` +
  207. `<div class="test">c</div>`,
  208. )
  209. // enter
  210. expect(await htmlWhenTransitionStart()).toBe(
  211. `<div class="test">a</div>` +
  212. `<div class="test">b</div>` +
  213. `<div class="test">c</div>` +
  214. `<div class="test test-enter-from test-enter-active">d</div>` +
  215. `<div class="test test-enter-from test-enter-active">e</div>`,
  216. )
  217. await nextFrame()
  218. expect(await html('#container')).toBe(
  219. `<div class="test">a</div>` +
  220. `<div class="test">b</div>` +
  221. `<div class="test">c</div>` +
  222. `<div class="test test-enter-active test-enter-to">d</div>` +
  223. `<div class="test test-enter-active test-enter-to">e</div>`,
  224. )
  225. await transitionFinish()
  226. expect(await html('#container')).toBe(
  227. `<div class="test">a</div>` +
  228. `<div class="test">b</div>` +
  229. `<div class="test">c</div>` +
  230. `<div class="test">d</div>` +
  231. `<div class="test">e</div>`,
  232. )
  233. },
  234. E2E_TIMEOUT,
  235. )
  236. test(
  237. 'move',
  238. async () => {
  239. await page().evaluate(() => {
  240. const { createApp, ref } = (window as any).Vue
  241. createApp({
  242. template: `
  243. <div id="container">
  244. <transition-group name="group">
  245. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  246. </transition-group>
  247. </div>
  248. <button id="toggleBtn" @click="click">button</button>
  249. `,
  250. setup: () => {
  251. const items = ref(['a', 'b', 'c'])
  252. const click = () => (items.value = ['d', 'b', 'a'])
  253. return { click, items }
  254. },
  255. }).mount('#app')
  256. })
  257. expect(await html('#container')).toBe(
  258. `<div class="test">a</div>` +
  259. `<div class="test">b</div>` +
  260. `<div class="test">c</div>`,
  261. )
  262. expect(await htmlWhenTransitionStart()).toBe(
  263. `<div class="test group-enter-from group-enter-active">d</div>` +
  264. `<div class="test">b</div>` +
  265. `<div class="test group-move" style="">a</div>` +
  266. `<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
  267. )
  268. await nextFrame()
  269. expect(await html('#container')).toBe(
  270. `<div class="test group-enter-active group-enter-to">d</div>` +
  271. `<div class="test">b</div>` +
  272. `<div class="test group-move" style="">a</div>` +
  273. `<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
  274. )
  275. await transitionFinish(duration * 2)
  276. expect(await html('#container')).toBe(
  277. `<div class="test">d</div>` +
  278. `<div class="test">b</div>` +
  279. `<div class="test" style="">a</div>`,
  280. )
  281. },
  282. E2E_TIMEOUT,
  283. )
  284. test(
  285. 'dynamic name',
  286. async () => {
  287. await page().evaluate(() => {
  288. const { createApp, ref } = (window as any).Vue
  289. createApp({
  290. template: `
  291. <div id="container">
  292. <transition-group :name="name">
  293. <div v-for="item in items" :key="item" >{{item}}</div>
  294. </transition-group>
  295. </div>
  296. <button id="toggleBtn" @click="click">button</button>
  297. <button id="changeNameBtn" @click="changeName">button</button>
  298. `,
  299. setup: () => {
  300. const items = ref(['a', 'b', 'c'])
  301. const name = ref('invalid')
  302. const click = () => (items.value = ['b', 'c', 'a'])
  303. const changeName = () => {
  304. name.value = 'group'
  305. items.value = ['a', 'b', 'c']
  306. }
  307. return { click, items, name, changeName }
  308. },
  309. }).mount('#app')
  310. })
  311. expect(await html('#container')).toBe(
  312. `<div>a</div>` + `<div>b</div>` + `<div>c</div>`,
  313. )
  314. // invalid name
  315. expect(await htmlWhenTransitionStart()).toBe(
  316. `<div>b</div>` + `<div>c</div>` + `<div>a</div>`,
  317. )
  318. // change name
  319. const moveHtml = await page().evaluate(() => {
  320. ;(document.querySelector('#changeNameBtn') as any).click()
  321. return Promise.resolve().then(() => {
  322. return document.querySelector('#container')!.innerHTML
  323. })
  324. })
  325. expect(moveHtml).toBe(
  326. `<div class="group-move" style="">a</div>` +
  327. `<div class="group-move" style="">b</div>` +
  328. `<div class="group-move" style="">c</div>`,
  329. )
  330. // not sure why but we just have to wait really long for this to
  331. // pass consistently :/
  332. await transitionFinish(duration * 4 + buffer)
  333. expect(await html('#container')).toBe(
  334. `<div class="" style="">a</div>` +
  335. `<div class="" style="">b</div>` +
  336. `<div class="" style="">c</div>`,
  337. )
  338. },
  339. E2E_TIMEOUT,
  340. )
  341. test(
  342. 'events',
  343. async () => {
  344. const onLeaveSpy = vi.fn()
  345. const onEnterSpy = vi.fn()
  346. const onAppearSpy = vi.fn()
  347. const beforeLeaveSpy = vi.fn()
  348. const beforeEnterSpy = vi.fn()
  349. const beforeAppearSpy = vi.fn()
  350. const afterLeaveSpy = vi.fn()
  351. const afterEnterSpy = vi.fn()
  352. const afterAppearSpy = vi.fn()
  353. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  354. await page().exposeFunction('onEnterSpy', onEnterSpy)
  355. await page().exposeFunction('onAppearSpy', onAppearSpy)
  356. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  357. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  358. await page().exposeFunction('beforeAppearSpy', beforeAppearSpy)
  359. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  360. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  361. await page().exposeFunction('afterAppearSpy', afterAppearSpy)
  362. const appearHtml = await page().evaluate(() => {
  363. const {
  364. beforeAppearSpy,
  365. onAppearSpy,
  366. afterAppearSpy,
  367. beforeEnterSpy,
  368. onEnterSpy,
  369. afterEnterSpy,
  370. beforeLeaveSpy,
  371. onLeaveSpy,
  372. afterLeaveSpy,
  373. } = window as any
  374. const { createApp, ref } = (window as any).Vue
  375. createApp({
  376. template: `
  377. <div id="container">
  378. <transition-group name="test"
  379. appear
  380. appear-from-class="test-appear-from"
  381. appear-to-class="test-appear-to"
  382. appear-active-class="test-appear-active"
  383. @before-enter="beforeEnterSpy()"
  384. @enter="onEnterSpy()"
  385. @after-enter="afterEnterSpy()"
  386. @before-leave="beforeLeaveSpy()"
  387. @leave="onLeaveSpy()"
  388. @after-leave="afterLeaveSpy()"
  389. @before-appear="beforeAppearSpy()"
  390. @appear="onAppearSpy()"
  391. @after-appear="afterAppearSpy()">
  392. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  393. </transition-group>
  394. </div>
  395. <button id="toggleBtn" @click="click">button</button>
  396. `,
  397. setup: () => {
  398. const items = ref(['a', 'b', 'c'])
  399. const click = () => (items.value = ['b', 'c', 'd'])
  400. return {
  401. click,
  402. items,
  403. beforeAppearSpy,
  404. onAppearSpy,
  405. afterAppearSpy,
  406. beforeEnterSpy,
  407. onEnterSpy,
  408. afterEnterSpy,
  409. beforeLeaveSpy,
  410. onLeaveSpy,
  411. afterLeaveSpy,
  412. }
  413. },
  414. }).mount('#app')
  415. return Promise.resolve().then(() => {
  416. return document.querySelector('#container')!.innerHTML
  417. })
  418. })
  419. expect(beforeAppearSpy).toBeCalled()
  420. expect(onAppearSpy).toBeCalled()
  421. expect(afterAppearSpy).not.toBeCalled()
  422. expect(appearHtml).toBe(
  423. `<div class="test test-appear-from test-appear-active">a</div>` +
  424. `<div class="test test-appear-from test-appear-active">b</div>` +
  425. `<div class="test test-appear-from test-appear-active">c</div>`,
  426. )
  427. await nextFrame()
  428. expect(afterAppearSpy).not.toBeCalled()
  429. expect(await html('#container')).toBe(
  430. `<div class="test test-appear-active test-appear-to">a</div>` +
  431. `<div class="test test-appear-active test-appear-to">b</div>` +
  432. `<div class="test test-appear-active test-appear-to">c</div>`,
  433. )
  434. await transitionFinish()
  435. expect(afterAppearSpy).toBeCalled()
  436. expect(await html('#container')).toBe(
  437. `<div class="test">a</div>` +
  438. `<div class="test">b</div>` +
  439. `<div class="test">c</div>`,
  440. )
  441. // enter + leave
  442. expect(await htmlWhenTransitionStart()).toBe(
  443. `<div class="test test-leave-from test-leave-active">a</div>` +
  444. `<div class="test">b</div>` +
  445. `<div class="test">c</div>` +
  446. `<div class="test test-enter-from test-enter-active">d</div>`,
  447. )
  448. expect(beforeLeaveSpy).toBeCalled()
  449. expect(onLeaveSpy).toBeCalled()
  450. expect(afterLeaveSpy).not.toBeCalled()
  451. expect(beforeEnterSpy).toBeCalled()
  452. expect(onEnterSpy).toBeCalled()
  453. expect(afterEnterSpy).not.toBeCalled()
  454. await nextFrame()
  455. expect(await html('#container')).toBe(
  456. `<div class="test test-leave-active test-leave-to">a</div>` +
  457. `<div class="test">b</div>` +
  458. `<div class="test">c</div>` +
  459. `<div class="test test-enter-active test-enter-to">d</div>`,
  460. )
  461. expect(afterLeaveSpy).not.toBeCalled()
  462. expect(afterEnterSpy).not.toBeCalled()
  463. await transitionFinish()
  464. expect(await html('#container')).toBe(
  465. `<div class="test">b</div>` +
  466. `<div class="test">c</div>` +
  467. `<div class="test">d</div>`,
  468. )
  469. expect(afterLeaveSpy).toBeCalled()
  470. expect(afterEnterSpy).toBeCalled()
  471. },
  472. E2E_TIMEOUT,
  473. )
  474. test('warn unkeyed children', () => {
  475. createApp({
  476. template: `
  477. <transition-group name="test">
  478. <div v-for="item in items" class="test">{{item}}</div>
  479. </transition-group>
  480. `,
  481. setup: () => {
  482. const items = ref(['a', 'b', 'c'])
  483. return { items }
  484. },
  485. }).mount(document.createElement('div'))
  486. expect(`<TransitionGroup> children must be keyed`).toHaveBeenWarned()
  487. })
  488. test('not warn unkeyed text children w/ whitespace preserve', () => {
  489. const app = createApp({
  490. template: `
  491. <transition-group name="test">
  492. <p key="1">1</p>
  493. <p key="2" v-if="false">2</p>
  494. </transition-group>
  495. `,
  496. })
  497. app.config.compilerOptions.whitespace = 'preserve'
  498. app.mount(document.createElement('div'))
  499. expect(`<TransitionGroup> children must be keyed`).not.toHaveBeenWarned()
  500. })
  501. // #5168, #7898, #9067
  502. test(
  503. 'avoid set transition hooks for comment node',
  504. async () => {
  505. await page().evaluate(duration => {
  506. const { createApp, ref, h, createCommentVNode } = (window as any).Vue
  507. const show = ref(false)
  508. createApp({
  509. template: `
  510. <div id="container">
  511. <transition-group name="test">
  512. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  513. <Child key="child"/>
  514. </transition-group>
  515. </div>
  516. <button id="toggleBtn" @click="click">button</button>
  517. `,
  518. components: {
  519. Child: {
  520. setup() {
  521. return () =>
  522. show.value
  523. ? h('div', { class: 'test' }, 'child')
  524. : createCommentVNode('v-if', true)
  525. },
  526. },
  527. },
  528. setup: () => {
  529. const items = ref([])
  530. const click = () => {
  531. items.value = ['a', 'b', 'c']
  532. setTimeout(() => {
  533. show.value = true
  534. }, duration)
  535. }
  536. return { click, items }
  537. },
  538. }).mount('#app')
  539. }, duration)
  540. expect(await html('#container')).toBe(`<!--v-if-->`)
  541. expect(await htmlWhenTransitionStart()).toBe(
  542. `<div class="test test-enter-from test-enter-active">a</div>` +
  543. `<div class="test test-enter-from test-enter-active">b</div>` +
  544. `<div class="test test-enter-from test-enter-active">c</div>` +
  545. `<!--v-if-->`,
  546. )
  547. await transitionFinish(duration)
  548. await nextFrame()
  549. expect(await html('#container')).toBe(
  550. `<div class="test">a</div>` +
  551. `<div class="test">b</div>` +
  552. `<div class="test">c</div>` +
  553. `<div class="test test-enter-active test-enter-to">child</div>`,
  554. )
  555. await transitionFinish(duration)
  556. expect(await html('#container')).toBe(
  557. `<div class="test">a</div>` +
  558. `<div class="test">b</div>` +
  559. `<div class="test">c</div>` +
  560. `<div class="test">child</div>`,
  561. )
  562. },
  563. E2E_TIMEOUT,
  564. )
  565. // #4621, #4622, #5153
  566. test(
  567. 'avoid set transition hooks for text node',
  568. async () => {
  569. await page().evaluate(() => {
  570. const { createApp, ref } = (window as any).Vue
  571. const app = createApp({
  572. template: `
  573. <div id="container">
  574. <transition-group name="test">
  575. <div class="test">foo</div>
  576. <div class="test" v-if="show">bar</div>
  577. </transition-group>
  578. </div>
  579. <button id="toggleBtn" @click="click">button</button>
  580. `,
  581. setup: () => {
  582. const show = ref(false)
  583. const click = () => {
  584. show.value = true
  585. }
  586. return { show, click }
  587. },
  588. })
  589. app.config.compilerOptions.whitespace = 'preserve'
  590. app.mount('#app')
  591. })
  592. expect(await html('#container')).toBe(`<div class="test">foo</div>` + ` `)
  593. expect(await htmlWhenTransitionStart()).toBe(
  594. `<div class="test">foo</div>` +
  595. ` ` +
  596. `<div class="test test-enter-from test-enter-active">bar</div>`,
  597. )
  598. await nextFrame()
  599. expect(await html('#container')).toBe(
  600. `<div class="test">foo</div>` +
  601. ` ` +
  602. `<div class="test test-enter-active test-enter-to">bar</div>`,
  603. )
  604. await transitionFinish(duration)
  605. expect(await html('#container')).toBe(
  606. `<div class="test">foo</div>` + ` ` + `<div class="test">bar</div>`,
  607. )
  608. },
  609. E2E_TIMEOUT,
  610. )
  611. })