TransitionGroup.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
  2. import path from 'path'
  3. import { mockWarn } from '@vue/shared'
  4. import { createApp, ref } from 'vue'
  5. describe('e2e: TransitionGroup', () => {
  6. mockWarn()
  7. const { page, html, nextFrame, timeout } = setupPuppeteer()
  8. const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
  9. const duration = 50
  10. const buffer = 5
  11. const htmlWhenTransitionStart = () =>
  12. page().evaluate(() => {
  13. (document.querySelector('#toggleBtn') as any)!.click()
  14. return Promise.resolve().then(() => {
  15. return document.querySelector('#container')!.innerHTML
  16. })
  17. })
  18. const transitionFinish = (time = duration) => timeout(time + buffer)
  19. beforeEach(async () => {
  20. await page().goto(baseUrl)
  21. await page().waitFor('#app')
  22. })
  23. test(
  24. 'enter',
  25. async () => {
  26. await page().evaluate(() => {
  27. const { createApp, ref } = (window as any).Vue
  28. createApp({
  29. template: `
  30. <div id="container">
  31. <transition-group name="test">
  32. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  33. </transition-group>
  34. </div>
  35. <button id="toggleBtn" @click="click">button</button>
  36. `,
  37. setup: () => {
  38. const items = ref(['a', 'b', 'c'])
  39. const click = () => items.value.push('d', 'e')
  40. return { click, items }
  41. }
  42. }).mount('#app')
  43. })
  44. expect(await html('#container')).toBe(
  45. `<div class="test">a</div>` +
  46. `<div class="test">b</div>` +
  47. `<div class="test">c</div>`
  48. )
  49. expect(await htmlWhenTransitionStart()).toBe(
  50. `<div class="test">a</div>` +
  51. `<div class="test">b</div>` +
  52. `<div class="test">c</div>` +
  53. `<div class="test test-enter-active test-enter-from">d</div>` +
  54. `<div class="test test-enter-active test-enter-from">e</div>`
  55. )
  56. await nextFrame()
  57. expect(await html('#container')).toBe(
  58. `<div class="test">a</div>` +
  59. `<div class="test">b</div>` +
  60. `<div class="test">c</div>` +
  61. `<div class="test test-enter-active test-enter-to">d</div>` +
  62. `<div class="test test-enter-active test-enter-to">e</div>`
  63. )
  64. await transitionFinish()
  65. expect(await html('#container')).toBe(
  66. `<div class="test">a</div>` +
  67. `<div class="test">b</div>` +
  68. `<div class="test">c</div>` +
  69. `<div class="test">d</div>` +
  70. `<div class="test">e</div>`
  71. )
  72. },
  73. E2E_TIMEOUT
  74. )
  75. test(
  76. 'leave',
  77. async () => {
  78. await page().evaluate(() => {
  79. const { createApp, ref } = (window as any).Vue
  80. createApp({
  81. template: `
  82. <div id="container">
  83. <transition-group name="test">
  84. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  85. </transition-group>
  86. </div>
  87. <button id="toggleBtn" @click="click">button</button>
  88. `,
  89. setup: () => {
  90. const items = ref(['a', 'b', 'c'])
  91. const click = () => (items.value = ['b'])
  92. return { click, items }
  93. }
  94. }).mount('#app')
  95. })
  96. expect(await html('#container')).toBe(
  97. `<div class="test">a</div>` +
  98. `<div class="test">b</div>` +
  99. `<div class="test">c</div>`
  100. )
  101. expect(await htmlWhenTransitionStart()).toBe(
  102. `<div class="test test-leave-active test-leave-from">a</div>` +
  103. `<div class="test">b</div>` +
  104. `<div class="test test-leave-active test-leave-from">c</div>`
  105. )
  106. await nextFrame()
  107. expect(await html('#container')).toBe(
  108. `<div class="test test-leave-active test-leave-to">a</div>` +
  109. `<div class="test">b</div>` +
  110. `<div class="test test-leave-active test-leave-to">c</div>`
  111. )
  112. await transitionFinish()
  113. expect(await html('#container')).toBe(`<div class="test">b</div>`)
  114. },
  115. E2E_TIMEOUT
  116. )
  117. test(
  118. 'enter + leave',
  119. async () => {
  120. await page().evaluate(() => {
  121. const { createApp, ref } = (window as any).Vue
  122. createApp({
  123. template: `
  124. <div id="container">
  125. <transition-group name="test">
  126. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  127. </transition-group>
  128. </div>
  129. <button id="toggleBtn" @click="click">button</button>
  130. `,
  131. setup: () => {
  132. const items = ref(['a', 'b', 'c'])
  133. const click = () => (items.value = ['b', 'c', 'd'])
  134. return { click, items }
  135. }
  136. }).mount('#app')
  137. })
  138. expect(await html('#container')).toBe(
  139. `<div class="test">a</div>` +
  140. `<div class="test">b</div>` +
  141. `<div class="test">c</div>`
  142. )
  143. expect(await htmlWhenTransitionStart()).toBe(
  144. `<div class="test test-leave-active test-leave-from">a</div>` +
  145. `<div class="test">b</div>` +
  146. `<div class="test">c</div>` +
  147. `<div class="test test-enter-active test-enter-from">d</div>`
  148. )
  149. await nextFrame()
  150. expect(await html('#container')).toBe(
  151. `<div class="test test-leave-active test-leave-to">a</div>` +
  152. `<div class="test">b</div>` +
  153. `<div class="test">c</div>` +
  154. `<div class="test test-enter-active test-enter-to">d</div>`
  155. )
  156. await transitionFinish()
  157. expect(await html('#container')).toBe(
  158. `<div class="test">b</div>` +
  159. `<div class="test">c</div>` +
  160. `<div class="test">d</div>`
  161. )
  162. },
  163. E2E_TIMEOUT
  164. )
  165. test(
  166. 'appear',
  167. async () => {
  168. const appearHtml = await page().evaluate(() => {
  169. const { createApp, ref } = (window as any).Vue
  170. createApp({
  171. template: `
  172. <div id="container">
  173. <transition-group appear
  174. appear-from-class="test-appear-from"
  175. appear-to-class="test-appear-to"
  176. appear-active-class="test-appear-active"
  177. name="test">
  178. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  179. </transition-group>
  180. </div>
  181. <button id="toggleBtn" @click="click">button</button>
  182. `,
  183. setup: () => {
  184. const items = ref(['a', 'b', 'c'])
  185. const click = () => items.value.push('d', 'e')
  186. return { click, items }
  187. }
  188. }).mount('#app')
  189. return Promise.resolve().then(() => {
  190. return document.querySelector('#container')!.innerHTML
  191. })
  192. })
  193. // appear
  194. expect(appearHtml).toBe(
  195. `<div class="test test-appear-active test-appear-from">a</div>` +
  196. `<div class="test test-appear-active test-appear-from">b</div>` +
  197. `<div class="test test-appear-active test-appear-from">c</div>`
  198. )
  199. await nextFrame()
  200. expect(await html('#container')).toBe(
  201. `<div class="test test-appear-active test-appear-to">a</div>` +
  202. `<div class="test test-appear-active test-appear-to">b</div>` +
  203. `<div class="test test-appear-active test-appear-to">c</div>`
  204. )
  205. await transitionFinish()
  206. expect(await html('#container')).toBe(
  207. `<div class="test">a</div>` +
  208. `<div class="test">b</div>` +
  209. `<div class="test">c</div>`
  210. )
  211. // enter
  212. expect(await htmlWhenTransitionStart()).toBe(
  213. `<div class="test">a</div>` +
  214. `<div class="test">b</div>` +
  215. `<div class="test">c</div>` +
  216. `<div class="test test-enter-active test-enter-from">d</div>` +
  217. `<div class="test test-enter-active test-enter-from">e</div>`
  218. )
  219. await nextFrame()
  220. expect(await html('#container')).toBe(
  221. `<div class="test">a</div>` +
  222. `<div class="test">b</div>` +
  223. `<div class="test">c</div>` +
  224. `<div class="test test-enter-active test-enter-to">d</div>` +
  225. `<div class="test test-enter-active test-enter-to">e</div>`
  226. )
  227. await transitionFinish()
  228. expect(await html('#container')).toBe(
  229. `<div class="test">a</div>` +
  230. `<div class="test">b</div>` +
  231. `<div class="test">c</div>` +
  232. `<div class="test">d</div>` +
  233. `<div class="test">e</div>`
  234. )
  235. },
  236. E2E_TIMEOUT
  237. )
  238. test(
  239. 'move',
  240. async () => {
  241. await page().evaluate(() => {
  242. const { createApp, ref } = (window as any).Vue
  243. createApp({
  244. template: `
  245. <div id="container">
  246. <transition-group name="group">
  247. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  248. </transition-group>
  249. </div>
  250. <button id="toggleBtn" @click="click">button</button>
  251. `,
  252. setup: () => {
  253. const items = ref(['a', 'b', 'c'])
  254. const click = () => (items.value = ['d', 'b', 'a'])
  255. return { click, items }
  256. }
  257. }).mount('#app')
  258. })
  259. expect(await html('#container')).toBe(
  260. `<div class="test">a</div>` +
  261. `<div class="test">b</div>` +
  262. `<div class="test">c</div>`
  263. )
  264. expect(await htmlWhenTransitionStart()).toBe(
  265. `<div class="test group-enter-active group-enter-from">d</div>` +
  266. `<div class="test">b</div>` +
  267. `<div class="test group-move" style="">a</div>` +
  268. `<div class="test group-leave-active group-leave-from group-move" style="">c</div>`
  269. )
  270. await nextFrame()
  271. expect(await html('#container')).toBe(
  272. `<div class="test group-enter-active group-enter-to">d</div>` +
  273. `<div class="test">b</div>` +
  274. `<div class="test group-move" style="">a</div>` +
  275. `<div class="test group-leave-active group-move group-leave-to" style="">c</div>`
  276. )
  277. await transitionFinish(duration * 2)
  278. expect(await html('#container')).toBe(
  279. `<div class="test">d</div>` +
  280. `<div class="test">b</div>` +
  281. `<div class="test" style="">a</div>`
  282. )
  283. },
  284. E2E_TIMEOUT
  285. )
  286. test(
  287. 'dynamic name',
  288. async () => {
  289. await page().evaluate(() => {
  290. const { createApp, ref } = (window as any).Vue
  291. createApp({
  292. template: `
  293. <div id="container">
  294. <transition-group :name="name">
  295. <div v-for="item in items" :key="item" >{{item}}</div>
  296. </transition-group>
  297. </div>
  298. <button id="toggleBtn" @click="click">button</button>
  299. <button id="changeNameBtn" @click="changeName">button</button>
  300. `,
  301. setup: () => {
  302. const items = ref(['a', 'b', 'c'])
  303. const name = ref('invalid')
  304. const click = () => (items.value = ['b', 'c', 'a'])
  305. const changeName = () => {
  306. name.value = 'group'
  307. items.value = ['a', 'b', 'c']
  308. }
  309. return { click, items, name, changeName }
  310. }
  311. }).mount('#app')
  312. })
  313. expect(await html('#container')).toBe(
  314. `<div>a</div>` + `<div>b</div>` + `<div>c</div>`
  315. )
  316. // invalid name
  317. expect(await htmlWhenTransitionStart()).toBe(
  318. `<div>b</div>` + `<div>c</div>` + `<div>a</div>`
  319. )
  320. // change name
  321. const moveHtml = await page().evaluate(() => {
  322. ;(document.querySelector('#changeNameBtn') as any).click()
  323. return Promise.resolve().then(() => {
  324. return document.querySelector('#container')!.innerHTML
  325. })
  326. })
  327. expect(moveHtml).toBe(
  328. `<div class="group-move" style="">a</div>` +
  329. `<div class="group-move" style="">b</div>` +
  330. `<div class="group-move" style="">c</div>`
  331. )
  332. // not sure why but we just have to wait really long for this to
  333. // pass consistently :/
  334. await transitionFinish(duration * 4)
  335. expect(await html('#container')).toBe(
  336. `<div class="" style="">a</div>` +
  337. `<div class="" style="">b</div>` +
  338. `<div class="" style="">c</div>`
  339. )
  340. },
  341. E2E_TIMEOUT
  342. )
  343. test(
  344. 'events',
  345. async () => {
  346. const onLeaveSpy = jest.fn()
  347. const onEnterSpy = jest.fn()
  348. const onAppearSpy = jest.fn()
  349. const beforeLeaveSpy = jest.fn()
  350. const beforeEnterSpy = jest.fn()
  351. const beforeAppearSpy = jest.fn()
  352. const afterLeaveSpy = jest.fn()
  353. const afterEnterSpy = jest.fn()
  354. const afterAppearSpy = jest.fn()
  355. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  356. await page().exposeFunction('onEnterSpy', onEnterSpy)
  357. await page().exposeFunction('onAppearSpy', onAppearSpy)
  358. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  359. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  360. await page().exposeFunction('beforeAppearSpy', beforeAppearSpy)
  361. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  362. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  363. await page().exposeFunction('afterAppearSpy', afterAppearSpy)
  364. const appearHtml = await page().evaluate(() => {
  365. const {
  366. beforeAppearSpy,
  367. onAppearSpy,
  368. afterAppearSpy,
  369. beforeEnterSpy,
  370. onEnterSpy,
  371. afterEnterSpy,
  372. beforeLeaveSpy,
  373. onLeaveSpy,
  374. afterLeaveSpy
  375. } = window as any
  376. const { createApp, ref } = (window as any).Vue
  377. createApp({
  378. template: `
  379. <div id="container">
  380. <transition-group name="test"
  381. appear
  382. appear-from-class="test-appear-from"
  383. appear-to-class="test-appear-to"
  384. appear-active-class="test-appear-active"
  385. @before-enter="beforeEnterSpy"
  386. @enter="onEnterSpy"
  387. @after-enter="afterEnterSpy"
  388. @before-leave="beforeLeaveSpy"
  389. @leave="onLeaveSpy"
  390. @after-leave="afterLeaveSpy"
  391. @before-appear="beforeAppearSpy"
  392. @appear="onAppearSpy"
  393. @after-appear="afterAppearSpy">
  394. <div v-for="item in items" :key="item" class="test">{{item}}</div>
  395. </transition-group>
  396. </div>
  397. <button id="toggleBtn" @click="click">button</button>
  398. `,
  399. setup: () => {
  400. const items = ref(['a', 'b', 'c'])
  401. const click = () => (items.value = ['b', 'c', 'd'])
  402. return {
  403. click,
  404. items,
  405. beforeAppearSpy,
  406. onAppearSpy,
  407. afterAppearSpy,
  408. beforeEnterSpy,
  409. onEnterSpy,
  410. afterEnterSpy,
  411. beforeLeaveSpy,
  412. onLeaveSpy,
  413. afterLeaveSpy
  414. }
  415. }
  416. }).mount('#app')
  417. return Promise.resolve().then(() => {
  418. return document.querySelector('#container')!.innerHTML
  419. })
  420. })
  421. expect(beforeAppearSpy).toBeCalled()
  422. expect(onAppearSpy).toBeCalled()
  423. expect(afterAppearSpy).not.toBeCalled()
  424. expect(appearHtml).toBe(
  425. `<div class="test test-appear-active test-appear-from">a</div>` +
  426. `<div class="test test-appear-active test-appear-from">b</div>` +
  427. `<div class="test test-appear-active test-appear-from">c</div>`
  428. )
  429. await nextFrame()
  430. expect(afterAppearSpy).not.toBeCalled()
  431. expect(await html('#container')).toBe(
  432. `<div class="test test-appear-active test-appear-to">a</div>` +
  433. `<div class="test test-appear-active test-appear-to">b</div>` +
  434. `<div class="test test-appear-active test-appear-to">c</div>`
  435. )
  436. await transitionFinish()
  437. expect(afterAppearSpy).toBeCalled()
  438. expect(await html('#container')).toBe(
  439. `<div class="test">a</div>` +
  440. `<div class="test">b</div>` +
  441. `<div class="test">c</div>`
  442. )
  443. // enter + leave
  444. expect(await htmlWhenTransitionStart()).toBe(
  445. `<div class="test test-leave-active test-leave-from">a</div>` +
  446. `<div class="test">b</div>` +
  447. `<div class="test">c</div>` +
  448. `<div class="test test-enter-active test-enter-from">d</div>`
  449. )
  450. expect(beforeLeaveSpy).toBeCalled()
  451. expect(onLeaveSpy).toBeCalled()
  452. expect(afterLeaveSpy).not.toBeCalled()
  453. expect(beforeEnterSpy).toBeCalled()
  454. expect(onEnterSpy).toBeCalled()
  455. expect(afterEnterSpy).not.toBeCalled()
  456. await nextFrame()
  457. expect(await html('#container')).toBe(
  458. `<div class="test test-leave-active test-leave-to">a</div>` +
  459. `<div class="test">b</div>` +
  460. `<div class="test">c</div>` +
  461. `<div class="test test-enter-active test-enter-to">d</div>`
  462. )
  463. expect(afterLeaveSpy).not.toBeCalled()
  464. expect(afterEnterSpy).not.toBeCalled()
  465. await transitionFinish()
  466. expect(await html('#container')).toBe(
  467. `<div class="test">b</div>` +
  468. `<div class="test">c</div>` +
  469. `<div class="test">d</div>`
  470. )
  471. expect(afterLeaveSpy).toBeCalled()
  472. expect(afterEnterSpy).toBeCalled()
  473. },
  474. E2E_TIMEOUT
  475. )
  476. test('warn unkeyed children', () => {
  477. createApp({
  478. template: `
  479. <transition-group name="test">
  480. <div v-for="item in items" class="test">{{item}}</div>
  481. </transition-group>
  482. `,
  483. setup: () => {
  484. const items = ref(['a', 'b', 'c'])
  485. return { items }
  486. }
  487. }).mount(document.createElement('div'))
  488. expect(`<TransitionGroup> children must be keyed`).toHaveBeenWarned()
  489. })
  490. })