Transition.spec.ts 97 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124
  1. import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
  2. import path from 'node:path'
  3. import { Transition, createApp, h, nextTick, ref } from 'vue'
  4. describe('e2e: Transition', () => {
  5. const { page, html, classList, isVisible, timeout, nextFrame, click } =
  6. setupPuppeteer()
  7. const baseUrl = `file://${path.resolve(__dirname, './transition.html')}`
  8. const duration = process.env.CI ? 200 : 50
  9. const buffer = process.env.CI ? 50 : 20
  10. const transitionFinish = (time = duration) => timeout(time + buffer)
  11. const classWhenTransitionStart = () =>
  12. page().evaluate(() => {
  13. ;(document.querySelector('#toggleBtn') as any)!.click()
  14. return Promise.resolve().then(() => {
  15. return document.querySelector('#container div')!.className.split(/\s+/g)
  16. })
  17. })
  18. beforeEach(async () => {
  19. await page().goto(baseUrl)
  20. await page().waitForSelector('#app')
  21. })
  22. describe('transition with v-if', () => {
  23. test(
  24. 'basic transition',
  25. async () => {
  26. await page().evaluate(() => {
  27. const { createApp, ref } = (window as any).Vue
  28. createApp({
  29. template: `
  30. <div id="container">
  31. <transition>
  32. <div v-if="toggle" class="test">content</div>
  33. </transition>
  34. </div>
  35. <button id="toggleBtn" @click="click">button</button>
  36. `,
  37. setup: () => {
  38. const toggle = ref(true)
  39. const click = () => (toggle.value = !toggle.value)
  40. return { toggle, click }
  41. },
  42. }).mount('#app')
  43. })
  44. expect(await html('#container')).toBe('<div class="test">content</div>')
  45. // leave
  46. expect(await classWhenTransitionStart()).toStrictEqual([
  47. 'test',
  48. 'v-leave-from',
  49. 'v-leave-active',
  50. ])
  51. await nextFrame()
  52. expect(await classList('.test')).toStrictEqual([
  53. 'test',
  54. 'v-leave-active',
  55. 'v-leave-to',
  56. ])
  57. await transitionFinish()
  58. expect(await html('#container')).toBe('<!--v-if-->')
  59. // enter
  60. expect(await classWhenTransitionStart()).toStrictEqual([
  61. 'test',
  62. 'v-enter-from',
  63. 'v-enter-active',
  64. ])
  65. await nextFrame()
  66. expect(await classList('.test')).toStrictEqual([
  67. 'test',
  68. 'v-enter-active',
  69. 'v-enter-to',
  70. ])
  71. await transitionFinish()
  72. expect(await html('#container')).toBe('<div class="test">content</div>')
  73. },
  74. E2E_TIMEOUT,
  75. )
  76. test(
  77. 'named transition',
  78. async () => {
  79. await page().evaluate(() => {
  80. const { createApp, ref } = (window as any).Vue
  81. createApp({
  82. template: `
  83. <div id="container">
  84. <transition name="test">
  85. <div v-if="toggle" class="test">content</div>
  86. </transition>
  87. </div>
  88. <button id="toggleBtn" @click="click">button</button>
  89. `,
  90. setup: () => {
  91. const toggle = ref(true)
  92. const click = () => (toggle.value = !toggle.value)
  93. return { toggle, click }
  94. },
  95. }).mount('#app')
  96. })
  97. expect(await html('#container')).toBe('<div class="test">content</div>')
  98. // leave
  99. expect(await classWhenTransitionStart()).toStrictEqual([
  100. 'test',
  101. 'test-leave-from',
  102. 'test-leave-active',
  103. ])
  104. await nextFrame()
  105. expect(await classList('.test')).toStrictEqual([
  106. 'test',
  107. 'test-leave-active',
  108. 'test-leave-to',
  109. ])
  110. await transitionFinish()
  111. expect(await html('#container')).toBe('<!--v-if-->')
  112. // enter
  113. expect(await classWhenTransitionStart()).toStrictEqual([
  114. 'test',
  115. 'test-enter-from',
  116. 'test-enter-active',
  117. ])
  118. await nextFrame()
  119. expect(await classList('.test')).toStrictEqual([
  120. 'test',
  121. 'test-enter-active',
  122. 'test-enter-to',
  123. ])
  124. await transitionFinish()
  125. expect(await html('#container')).toBe('<div class="test">content</div>')
  126. },
  127. E2E_TIMEOUT,
  128. )
  129. test(
  130. 'custom transition classes',
  131. async () => {
  132. await page().evaluate(() => {
  133. const { createApp, ref } = (window as any).Vue
  134. createApp({
  135. template: `
  136. <div id="container">
  137. <transition enter-from-class="hello-from"
  138. enter-active-class="hello-active"
  139. enter-to-class="hello-to"
  140. leave-from-class="bye-from"
  141. leave-active-class="bye-active"
  142. leave-to-class="bye-to">
  143. <div v-if="toggle" class="test">content</div>
  144. </transition>
  145. </div>
  146. <button id="toggleBtn" @click="click">button</button>
  147. `,
  148. setup: () => {
  149. const toggle = ref(true)
  150. const click = () => (toggle.value = !toggle.value)
  151. return { toggle, click }
  152. },
  153. }).mount('#app')
  154. })
  155. expect(await html('#container')).toBe('<div class="test">content</div>')
  156. // leave
  157. expect(await classWhenTransitionStart()).toStrictEqual([
  158. 'test',
  159. 'bye-from',
  160. 'bye-active',
  161. ])
  162. await nextFrame()
  163. expect(await classList('.test')).toStrictEqual([
  164. 'test',
  165. 'bye-active',
  166. 'bye-to',
  167. ])
  168. await transitionFinish()
  169. expect(await html('#container')).toBe('<!--v-if-->')
  170. // enter
  171. expect(await classWhenTransitionStart()).toStrictEqual([
  172. 'test',
  173. 'hello-from',
  174. 'hello-active',
  175. ])
  176. await nextFrame()
  177. expect(await classList('.test')).toStrictEqual([
  178. 'test',
  179. 'hello-active',
  180. 'hello-to',
  181. ])
  182. await transitionFinish()
  183. expect(await html('#container')).toBe('<div class="test">content</div>')
  184. },
  185. E2E_TIMEOUT,
  186. )
  187. test(
  188. 'transition with dynamic name',
  189. async () => {
  190. await page().evaluate(() => {
  191. const { createApp, ref } = (window as any).Vue
  192. createApp({
  193. template: `
  194. <div id="container">
  195. <transition :name="name">
  196. <div v-if="toggle" class="test">content</div>
  197. </transition>
  198. </div>
  199. <button id="toggleBtn" @click="click">button</button>
  200. <button id="changeNameBtn" @click="changeName">button</button>
  201. `,
  202. setup: () => {
  203. const name = ref('test')
  204. const toggle = ref(true)
  205. const click = () => (toggle.value = !toggle.value)
  206. const changeName = () => (name.value = 'changed')
  207. return { toggle, click, name, changeName }
  208. },
  209. }).mount('#app')
  210. })
  211. expect(await html('#container')).toBe('<div class="test">content</div>')
  212. // leave
  213. expect(await classWhenTransitionStart()).toStrictEqual([
  214. 'test',
  215. 'test-leave-from',
  216. 'test-leave-active',
  217. ])
  218. await nextFrame()
  219. expect(await classList('.test')).toStrictEqual([
  220. 'test',
  221. 'test-leave-active',
  222. 'test-leave-to',
  223. ])
  224. await transitionFinish()
  225. expect(await html('#container')).toBe('<!--v-if-->')
  226. // enter
  227. await page().evaluate(() => {
  228. ;(document.querySelector('#changeNameBtn') as any).click()
  229. })
  230. expect(await classWhenTransitionStart()).toStrictEqual([
  231. 'test',
  232. 'changed-enter-from',
  233. 'changed-enter-active',
  234. ])
  235. await nextFrame()
  236. expect(await classList('.test')).toStrictEqual([
  237. 'test',
  238. 'changed-enter-active',
  239. 'changed-enter-to',
  240. ])
  241. await transitionFinish()
  242. expect(await html('#container')).toBe('<div class="test">content</div>')
  243. },
  244. E2E_TIMEOUT,
  245. )
  246. test(
  247. 'transition events without appear',
  248. async () => {
  249. const beforeLeaveSpy = vi.fn()
  250. const onLeaveSpy = vi.fn()
  251. const afterLeaveSpy = vi.fn()
  252. const beforeEnterSpy = vi.fn()
  253. const onEnterSpy = vi.fn()
  254. const afterEnterSpy = vi.fn()
  255. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  256. await page().exposeFunction('onEnterSpy', onEnterSpy)
  257. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  258. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  259. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  260. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  261. await page().evaluate(() => {
  262. const {
  263. beforeEnterSpy,
  264. onEnterSpy,
  265. afterEnterSpy,
  266. beforeLeaveSpy,
  267. onLeaveSpy,
  268. afterLeaveSpy,
  269. } = window as any
  270. const { createApp, ref } = (window as any).Vue
  271. createApp({
  272. template: `
  273. <div id="container">
  274. <transition
  275. name="test"
  276. @before-enter="beforeEnterSpy()"
  277. @enter="onEnterSpy()"
  278. @after-enter="afterEnterSpy()"
  279. @before-leave="beforeLeaveSpy()"
  280. @leave="onLeaveSpy()"
  281. @after-leave="afterLeaveSpy()">
  282. <div v-if="toggle" class="test">content</div>
  283. </transition>
  284. </div>
  285. <button id="toggleBtn" @click="click">button</button>
  286. `,
  287. setup: () => {
  288. const toggle = ref(true)
  289. const click = () => (toggle.value = !toggle.value)
  290. return {
  291. toggle,
  292. click,
  293. beforeEnterSpy,
  294. onEnterSpy,
  295. afterEnterSpy,
  296. beforeLeaveSpy,
  297. onLeaveSpy,
  298. afterLeaveSpy,
  299. }
  300. },
  301. }).mount('#app')
  302. })
  303. expect(await html('#container')).toBe('<div class="test">content</div>')
  304. // leave
  305. expect(await classWhenTransitionStart()).toStrictEqual([
  306. 'test',
  307. 'test-leave-from',
  308. 'test-leave-active',
  309. ])
  310. expect(beforeLeaveSpy).toBeCalled()
  311. expect(onLeaveSpy).toBeCalled()
  312. expect(afterLeaveSpy).not.toBeCalled()
  313. await nextFrame()
  314. expect(await classList('.test')).toStrictEqual([
  315. 'test',
  316. 'test-leave-active',
  317. 'test-leave-to',
  318. ])
  319. expect(afterLeaveSpy).not.toBeCalled()
  320. await transitionFinish()
  321. expect(await html('#container')).toBe('<!--v-if-->')
  322. expect(afterLeaveSpy).toBeCalled()
  323. // enter
  324. expect(await classWhenTransitionStart()).toStrictEqual([
  325. 'test',
  326. 'test-enter-from',
  327. 'test-enter-active',
  328. ])
  329. expect(beforeEnterSpy).toBeCalled()
  330. expect(onEnterSpy).toBeCalled()
  331. expect(afterEnterSpy).not.toBeCalled()
  332. await nextFrame()
  333. expect(await classList('.test')).toStrictEqual([
  334. 'test',
  335. 'test-enter-active',
  336. 'test-enter-to',
  337. ])
  338. expect(afterEnterSpy).not.toBeCalled()
  339. await transitionFinish()
  340. expect(await html('#container')).toBe('<div class="test">content</div>')
  341. expect(afterEnterSpy).toBeCalled()
  342. },
  343. E2E_TIMEOUT,
  344. )
  345. test(
  346. 'events with arguments',
  347. async () => {
  348. const beforeLeaveSpy = vi.fn()
  349. const onLeaveSpy = vi.fn()
  350. const afterLeaveSpy = vi.fn()
  351. const beforeEnterSpy = vi.fn()
  352. const onEnterSpy = vi.fn()
  353. const afterEnterSpy = vi.fn()
  354. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  355. await page().exposeFunction('onEnterSpy', onEnterSpy)
  356. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  357. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  358. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  359. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  360. await page().evaluate(() => {
  361. const {
  362. beforeEnterSpy,
  363. onEnterSpy,
  364. afterEnterSpy,
  365. beforeLeaveSpy,
  366. onLeaveSpy,
  367. afterLeaveSpy,
  368. } = window as any
  369. const { createApp, ref } = (window as any).Vue
  370. createApp({
  371. template: `
  372. <div id="container">
  373. <transition
  374. :css="false"
  375. name="test"
  376. @before-enter="beforeEnterSpy"
  377. @enter="onEnterSpy"
  378. @after-enter="afterEnterSpy"
  379. @before-leave="beforeLeaveSpy"
  380. @leave="onLeaveSpy"
  381. @after-leave="afterLeaveSpy">
  382. <div v-if="toggle" class="test">content</div>
  383. </transition>
  384. </div>
  385. <button id="toggleBtn" @click="click">button</button>
  386. `,
  387. setup: () => {
  388. const toggle = ref(true)
  389. const click = () => (toggle.value = !toggle.value)
  390. return {
  391. toggle,
  392. click,
  393. beforeEnterSpy(el: Element) {
  394. beforeEnterSpy()
  395. el.classList.add('before-enter')
  396. },
  397. onEnterSpy(el: Element, done: () => void) {
  398. onEnterSpy()
  399. el.classList.add('enter')
  400. setTimeout(done, 200)
  401. },
  402. afterEnterSpy(el: Element) {
  403. afterEnterSpy()
  404. el.classList.add('after-enter')
  405. },
  406. beforeLeaveSpy(el: HTMLDivElement) {
  407. beforeLeaveSpy()
  408. el.classList.add('before-leave')
  409. },
  410. onLeaveSpy(el: HTMLDivElement, done: () => void) {
  411. onLeaveSpy()
  412. el.classList.add('leave')
  413. setTimeout(done, 200)
  414. },
  415. afterLeaveSpy: (el: Element) => {
  416. afterLeaveSpy()
  417. },
  418. }
  419. },
  420. }).mount('#app')
  421. })
  422. expect(await html('#container')).toBe('<div class="test">content</div>')
  423. // leave
  424. await click('#toggleBtn')
  425. expect(beforeLeaveSpy).toBeCalled()
  426. expect(onLeaveSpy).toBeCalled()
  427. expect(afterLeaveSpy).not.toBeCalled()
  428. expect(await classList('.test')).toStrictEqual([
  429. 'test',
  430. 'before-leave',
  431. 'leave',
  432. ])
  433. await timeout(200 + buffer)
  434. expect(afterLeaveSpy).toBeCalled()
  435. expect(await html('#container')).toBe('<!--v-if-->')
  436. // enter
  437. await click('#toggleBtn')
  438. expect(beforeEnterSpy).toBeCalled()
  439. expect(onEnterSpy).toBeCalled()
  440. expect(afterEnterSpy).not.toBeCalled()
  441. expect(await classList('.test')).toStrictEqual([
  442. 'test',
  443. 'before-enter',
  444. 'enter',
  445. ])
  446. await timeout(200 + buffer)
  447. expect(afterEnterSpy).toBeCalled()
  448. expect(await html('#container')).toBe(
  449. '<div class="test before-enter enter after-enter">content</div>',
  450. )
  451. },
  452. E2E_TIMEOUT,
  453. )
  454. test('onEnterCancelled', async () => {
  455. const enterCancelledSpy = vi.fn()
  456. await page().exposeFunction('enterCancelledSpy', enterCancelledSpy)
  457. await page().evaluate(() => {
  458. const { enterCancelledSpy } = window as any
  459. const { createApp, ref } = (window as any).Vue
  460. createApp({
  461. template: `
  462. <div id="container">
  463. <transition
  464. name="test"
  465. @enter-cancelled="enterCancelledSpy()">
  466. <div v-if="toggle" class="test">content</div>
  467. </transition>
  468. </div>
  469. <button id="toggleBtn" @click="click">button</button>
  470. `,
  471. setup: () => {
  472. const toggle = ref(false)
  473. const click = () => (toggle.value = !toggle.value)
  474. return {
  475. toggle,
  476. click,
  477. enterCancelledSpy,
  478. }
  479. },
  480. }).mount('#app')
  481. })
  482. expect(await html('#container')).toBe('<!--v-if-->')
  483. // enter
  484. expect(await classWhenTransitionStart()).toStrictEqual([
  485. 'test',
  486. 'test-enter-from',
  487. 'test-enter-active',
  488. ])
  489. await nextFrame()
  490. expect(await classList('.test')).toStrictEqual([
  491. 'test',
  492. 'test-enter-active',
  493. 'test-enter-to',
  494. ])
  495. // cancel (leave)
  496. expect(await classWhenTransitionStart()).toStrictEqual([
  497. 'test',
  498. 'test-leave-from',
  499. 'test-leave-active',
  500. ])
  501. expect(enterCancelledSpy).toBeCalled()
  502. await nextFrame()
  503. expect(await classList('.test')).toStrictEqual([
  504. 'test',
  505. 'test-leave-active',
  506. 'test-leave-to',
  507. ])
  508. await transitionFinish()
  509. expect(await html('#container')).toBe('<!--v-if-->')
  510. })
  511. test(
  512. 'transition on appear',
  513. async () => {
  514. const appearClass = await page().evaluate(async () => {
  515. const { createApp, ref } = (window as any).Vue
  516. createApp({
  517. template: `
  518. <div id="container">
  519. <transition name="test"
  520. appear
  521. appear-from-class="test-appear-from"
  522. appear-to-class="test-appear-to"
  523. appear-active-class="test-appear-active">
  524. <div v-if="toggle" class="test">content</div>
  525. </transition>
  526. </div>
  527. <button id="toggleBtn" @click="click">button</button>
  528. `,
  529. setup: () => {
  530. const toggle = ref(true)
  531. const click = () => (toggle.value = !toggle.value)
  532. return { toggle, click }
  533. },
  534. }).mount('#app')
  535. return Promise.resolve().then(() => {
  536. return document.querySelector('.test')!.className.split(/\s+/g)
  537. })
  538. })
  539. // appear
  540. expect(appearClass).toStrictEqual([
  541. 'test',
  542. 'test-appear-from',
  543. 'test-appear-active',
  544. ])
  545. await nextFrame()
  546. expect(await classList('.test')).toStrictEqual([
  547. 'test',
  548. 'test-appear-active',
  549. 'test-appear-to',
  550. ])
  551. await transitionFinish()
  552. expect(await html('#container')).toBe('<div class="test">content</div>')
  553. // leave
  554. expect(await classWhenTransitionStart()).toStrictEqual([
  555. 'test',
  556. 'test-leave-from',
  557. 'test-leave-active',
  558. ])
  559. await nextFrame()
  560. expect(await classList('.test')).toStrictEqual([
  561. 'test',
  562. 'test-leave-active',
  563. 'test-leave-to',
  564. ])
  565. await transitionFinish()
  566. expect(await html('#container')).toBe('<!--v-if-->')
  567. // enter
  568. expect(await classWhenTransitionStart()).toStrictEqual([
  569. 'test',
  570. 'test-enter-from',
  571. 'test-enter-active',
  572. ])
  573. await nextFrame()
  574. expect(await classList('.test')).toStrictEqual([
  575. 'test',
  576. 'test-enter-active',
  577. 'test-enter-to',
  578. ])
  579. await transitionFinish()
  580. expect(await html('#container')).toBe('<div class="test">content</div>')
  581. },
  582. E2E_TIMEOUT,
  583. )
  584. test(
  585. 'transition events with appear',
  586. async () => {
  587. const onLeaveSpy = vi.fn()
  588. const onEnterSpy = vi.fn()
  589. const onAppearSpy = vi.fn()
  590. const beforeLeaveSpy = vi.fn()
  591. const beforeEnterSpy = vi.fn()
  592. const beforeAppearSpy = vi.fn()
  593. const afterLeaveSpy = vi.fn()
  594. const afterEnterSpy = vi.fn()
  595. const afterAppearSpy = vi.fn()
  596. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  597. await page().exposeFunction('onEnterSpy', onEnterSpy)
  598. await page().exposeFunction('onAppearSpy', onAppearSpy)
  599. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  600. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  601. await page().exposeFunction('beforeAppearSpy', beforeAppearSpy)
  602. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  603. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  604. await page().exposeFunction('afterAppearSpy', afterAppearSpy)
  605. const appearClass = await page().evaluate(async () => {
  606. const {
  607. beforeAppearSpy,
  608. onAppearSpy,
  609. afterAppearSpy,
  610. beforeEnterSpy,
  611. onEnterSpy,
  612. afterEnterSpy,
  613. beforeLeaveSpy,
  614. onLeaveSpy,
  615. afterLeaveSpy,
  616. } = window as any
  617. const { createApp, ref } = (window as any).Vue
  618. createApp({
  619. template: `
  620. <div id="container">
  621. <transition
  622. name="test"
  623. appear
  624. appear-from-class="test-appear-from"
  625. appear-to-class="test-appear-to"
  626. appear-active-class="test-appear-active"
  627. @before-enter="beforeEnterSpy()"
  628. @enter="onEnterSpy()"
  629. @after-enter="afterEnterSpy()"
  630. @before-leave="beforeLeaveSpy()"
  631. @leave="onLeaveSpy()"
  632. @after-leave="afterLeaveSpy()"
  633. @before-appear="beforeAppearSpy()"
  634. @appear="onAppearSpy()"
  635. @after-appear="afterAppearSpy()">
  636. <div v-if="toggle" class="test">content</div>
  637. </transition>
  638. </div>
  639. <button id="toggleBtn" @click="click">button</button>
  640. `,
  641. setup: () => {
  642. const toggle = ref(true)
  643. const click = () => (toggle.value = !toggle.value)
  644. return {
  645. toggle,
  646. click,
  647. beforeAppearSpy,
  648. onAppearSpy,
  649. afterAppearSpy,
  650. beforeEnterSpy,
  651. onEnterSpy,
  652. afterEnterSpy,
  653. beforeLeaveSpy,
  654. onLeaveSpy,
  655. afterLeaveSpy,
  656. }
  657. },
  658. }).mount('#app')
  659. return Promise.resolve().then(() => {
  660. return document.querySelector('.test')!.className.split(/\s+/g)
  661. })
  662. })
  663. // appear
  664. expect(appearClass).toStrictEqual([
  665. 'test',
  666. 'test-appear-from',
  667. 'test-appear-active',
  668. ])
  669. expect(beforeAppearSpy).toBeCalled()
  670. expect(onAppearSpy).toBeCalled()
  671. expect(afterAppearSpy).not.toBeCalled()
  672. await nextFrame()
  673. expect(await classList('.test')).toStrictEqual([
  674. 'test',
  675. 'test-appear-active',
  676. 'test-appear-to',
  677. ])
  678. expect(afterAppearSpy).not.toBeCalled()
  679. await transitionFinish()
  680. expect(await html('#container')).toBe('<div class="test">content</div>')
  681. expect(afterAppearSpy).toBeCalled()
  682. expect(beforeEnterSpy).not.toBeCalled()
  683. expect(onEnterSpy).not.toBeCalled()
  684. expect(afterEnterSpy).not.toBeCalled()
  685. // leave
  686. expect(await classWhenTransitionStart()).toStrictEqual([
  687. 'test',
  688. 'test-leave-from',
  689. 'test-leave-active',
  690. ])
  691. expect(beforeLeaveSpy).toBeCalled()
  692. expect(onLeaveSpy).toBeCalled()
  693. expect(afterLeaveSpy).not.toBeCalled()
  694. await nextFrame()
  695. expect(await classList('.test')).toStrictEqual([
  696. 'test',
  697. 'test-leave-active',
  698. 'test-leave-to',
  699. ])
  700. expect(afterLeaveSpy).not.toBeCalled()
  701. await transitionFinish()
  702. expect(await html('#container')).toBe('<!--v-if-->')
  703. expect(afterLeaveSpy).toBeCalled()
  704. // enter
  705. expect(await classWhenTransitionStart()).toStrictEqual([
  706. 'test',
  707. 'test-enter-from',
  708. 'test-enter-active',
  709. ])
  710. expect(beforeEnterSpy).toBeCalled()
  711. expect(onEnterSpy).toBeCalled()
  712. expect(afterEnterSpy).not.toBeCalled()
  713. await nextFrame()
  714. expect(await classList('.test')).toStrictEqual([
  715. 'test',
  716. 'test-enter-active',
  717. 'test-enter-to',
  718. ])
  719. expect(afterEnterSpy).not.toBeCalled()
  720. await transitionFinish()
  721. expect(await html('#container')).toBe('<div class="test">content</div>')
  722. expect(afterEnterSpy).toBeCalled()
  723. },
  724. E2E_TIMEOUT,
  725. )
  726. test(
  727. 'css: false',
  728. async () => {
  729. const onBeforeEnterSpy = vi.fn()
  730. const onEnterSpy = vi.fn()
  731. const onAfterEnterSpy = vi.fn()
  732. const onBeforeLeaveSpy = vi.fn()
  733. const onLeaveSpy = vi.fn()
  734. const onAfterLeaveSpy = vi.fn()
  735. await page().exposeFunction('onBeforeEnterSpy', onBeforeEnterSpy)
  736. await page().exposeFunction('onEnterSpy', onEnterSpy)
  737. await page().exposeFunction('onAfterEnterSpy', onAfterEnterSpy)
  738. await page().exposeFunction('onBeforeLeaveSpy', onBeforeLeaveSpy)
  739. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  740. await page().exposeFunction('onAfterLeaveSpy', onAfterLeaveSpy)
  741. await page().evaluate(() => {
  742. const {
  743. onBeforeEnterSpy,
  744. onEnterSpy,
  745. onAfterEnterSpy,
  746. onBeforeLeaveSpy,
  747. onLeaveSpy,
  748. onAfterLeaveSpy,
  749. } = window as any
  750. const { createApp, ref } = (window as any).Vue
  751. createApp({
  752. template: `
  753. <div id="container">
  754. <transition
  755. :css="false"
  756. name="test"
  757. @before-enter="onBeforeEnterSpy()"
  758. @enter="onEnterSpy()"
  759. @after-enter="onAfterEnterSpy()"
  760. @before-leave="onBeforeLeaveSpy()"
  761. @leave="onLeaveSpy()"
  762. @after-leave="onAfterLeaveSpy()">
  763. <div v-if="toggle" class="test">content</div>
  764. </transition>
  765. </div>
  766. <button id="toggleBtn" @click="click"></button>
  767. `,
  768. setup: () => {
  769. const toggle = ref(true)
  770. const click = () => (toggle.value = !toggle.value)
  771. return {
  772. toggle,
  773. click,
  774. onBeforeEnterSpy,
  775. onEnterSpy,
  776. onAfterEnterSpy,
  777. onBeforeLeaveSpy,
  778. onLeaveSpy,
  779. onAfterLeaveSpy,
  780. }
  781. },
  782. }).mount('#app')
  783. })
  784. expect(await html('#container')).toBe('<div class="test">content</div>')
  785. // leave
  786. await click('#toggleBtn')
  787. expect(onBeforeLeaveSpy).toBeCalled()
  788. expect(onLeaveSpy).toBeCalled()
  789. expect(onAfterLeaveSpy).toBeCalled()
  790. expect(await html('#container')).toBe('<!--v-if-->')
  791. // enter
  792. await classWhenTransitionStart()
  793. expect(onBeforeEnterSpy).toBeCalled()
  794. expect(onEnterSpy).toBeCalled()
  795. expect(onAfterEnterSpy).toBeCalled()
  796. expect(await html('#container')).toBe('<div class="test">content</div>')
  797. },
  798. E2E_TIMEOUT,
  799. )
  800. test(
  801. 'no transition detected',
  802. async () => {
  803. await page().evaluate(() => {
  804. const { createApp, ref } = (window as any).Vue
  805. createApp({
  806. template: `
  807. <div id="container">
  808. <transition name="noop">
  809. <div v-if="toggle">content</div>
  810. </transition>
  811. </div>
  812. <button id="toggleBtn" @click="click">button</button>
  813. `,
  814. setup: () => {
  815. const toggle = ref(true)
  816. const click = () => (toggle.value = !toggle.value)
  817. return { toggle, click }
  818. },
  819. }).mount('#app')
  820. })
  821. expect(await html('#container')).toBe('<div>content</div>')
  822. // leave
  823. expect(await classWhenTransitionStart()).toStrictEqual([
  824. 'noop-leave-from',
  825. 'noop-leave-active',
  826. ])
  827. await nextFrame()
  828. expect(await html('#container')).toBe('<!--v-if-->')
  829. // enter
  830. expect(await classWhenTransitionStart()).toStrictEqual([
  831. 'noop-enter-from',
  832. 'noop-enter-active',
  833. ])
  834. await nextFrame()
  835. expect(await html('#container')).toBe('<div class="">content</div>')
  836. },
  837. E2E_TIMEOUT,
  838. )
  839. test(
  840. 'animations',
  841. async () => {
  842. await page().evaluate(() => {
  843. const { createApp, ref } = (window as any).Vue
  844. createApp({
  845. template: `
  846. <div id="container">
  847. <transition name="test-anim">
  848. <div v-if="toggle">content</div>
  849. </transition>
  850. </div>
  851. <button id="toggleBtn" @click="click">button</button>
  852. `,
  853. setup: () => {
  854. const toggle = ref(true)
  855. const click = () => (toggle.value = !toggle.value)
  856. return { toggle, click }
  857. },
  858. }).mount('#app')
  859. })
  860. expect(await html('#container')).toBe('<div>content</div>')
  861. // leave
  862. expect(await classWhenTransitionStart()).toStrictEqual([
  863. 'test-anim-leave-from',
  864. 'test-anim-leave-active',
  865. ])
  866. await nextFrame()
  867. expect(await classList('#container div')).toStrictEqual([
  868. 'test-anim-leave-active',
  869. 'test-anim-leave-to',
  870. ])
  871. await transitionFinish(duration * 2)
  872. expect(await html('#container')).toBe('<!--v-if-->')
  873. // enter
  874. expect(await classWhenTransitionStart()).toStrictEqual([
  875. 'test-anim-enter-from',
  876. 'test-anim-enter-active',
  877. ])
  878. await nextFrame()
  879. expect(await classList('#container div')).toStrictEqual([
  880. 'test-anim-enter-active',
  881. 'test-anim-enter-to',
  882. ])
  883. await transitionFinish()
  884. expect(await html('#container')).toBe('<div class="">content</div>')
  885. },
  886. E2E_TIMEOUT,
  887. )
  888. test(
  889. 'explicit transition type',
  890. async () => {
  891. await page().evaluate(() => {
  892. const { createApp, ref } = (window as any).Vue
  893. createApp({
  894. template: `
  895. <div id="container"><transition name="test-anim-long" type="animation"><div v-if="toggle">content</div></transition></div>
  896. <button id="toggleBtn" @click="click">button</button>
  897. `,
  898. setup: () => {
  899. const toggle = ref(true)
  900. const click = () => (toggle.value = !toggle.value)
  901. return { toggle, click }
  902. },
  903. }).mount('#app')
  904. })
  905. expect(await html('#container')).toBe('<div>content</div>')
  906. // leave
  907. expect(await classWhenTransitionStart()).toStrictEqual([
  908. 'test-anim-long-leave-from',
  909. 'test-anim-long-leave-active',
  910. ])
  911. await nextFrame()
  912. expect(await classList('#container div')).toStrictEqual([
  913. 'test-anim-long-leave-active',
  914. 'test-anim-long-leave-to',
  915. ])
  916. if (!process.env.CI) {
  917. await new Promise(r => {
  918. setTimeout(r, duration - buffer)
  919. })
  920. expect(await classList('#container div')).toStrictEqual([
  921. 'test-anim-long-leave-active',
  922. 'test-anim-long-leave-to',
  923. ])
  924. }
  925. await transitionFinish(duration * 2)
  926. expect(await html('#container')).toBe('<!--v-if-->')
  927. // enter
  928. expect(await classWhenTransitionStart()).toStrictEqual([
  929. 'test-anim-long-enter-from',
  930. 'test-anim-long-enter-active',
  931. ])
  932. await nextFrame()
  933. expect(await classList('#container div')).toStrictEqual([
  934. 'test-anim-long-enter-active',
  935. 'test-anim-long-enter-to',
  936. ])
  937. if (!process.env.CI) {
  938. await new Promise(r => {
  939. setTimeout(r, duration - buffer)
  940. })
  941. expect(await classList('#container div')).toStrictEqual([
  942. 'test-anim-long-enter-active',
  943. 'test-anim-long-enter-to',
  944. ])
  945. }
  946. await transitionFinish(duration * 2)
  947. expect(await html('#container')).toBe('<div class="">content</div>')
  948. },
  949. E2E_TIMEOUT,
  950. )
  951. test(
  952. 'transition on SVG elements',
  953. async () => {
  954. await page().evaluate(() => {
  955. const { createApp, ref } = (window as any).Vue
  956. createApp({
  957. template: `
  958. <svg id="container">
  959. <transition name="test">
  960. <circle v-if="toggle" cx="0" cy="0" r="10" class="test"></circle>
  961. </transition>
  962. </svg>
  963. <button id="toggleBtn" @click="click">button</button>
  964. `,
  965. setup: () => {
  966. const toggle = ref(true)
  967. const click = () => (toggle.value = !toggle.value)
  968. return { toggle, click }
  969. },
  970. }).mount('#app')
  971. })
  972. expect(await html('#container')).toBe(
  973. '<circle cx="0" cy="0" r="10" class="test"></circle>',
  974. )
  975. const svgTransitionStart = () =>
  976. page().evaluate(() => {
  977. document.querySelector('button')!.click()
  978. return Promise.resolve().then(() => {
  979. return document
  980. .querySelector('.test')!
  981. .getAttribute('class')!
  982. .split(/\s+/g)
  983. })
  984. })
  985. // leave
  986. expect(await svgTransitionStart()).toStrictEqual([
  987. 'test',
  988. 'test-leave-from',
  989. 'test-leave-active',
  990. ])
  991. await nextFrame()
  992. expect(await classList('.test')).toStrictEqual([
  993. 'test',
  994. 'test-leave-active',
  995. 'test-leave-to',
  996. ])
  997. await transitionFinish()
  998. expect(await html('#container')).toBe('<!--v-if-->')
  999. // enter
  1000. expect(await svgTransitionStart()).toStrictEqual([
  1001. 'test',
  1002. 'test-enter-from',
  1003. 'test-enter-active',
  1004. ])
  1005. await nextFrame()
  1006. expect(await classList('.test')).toStrictEqual([
  1007. 'test',
  1008. 'test-enter-active',
  1009. 'test-enter-to',
  1010. ])
  1011. await transitionFinish()
  1012. expect(await html('#container')).toBe(
  1013. '<circle cx="0" cy="0" r="10" class="test"></circle>',
  1014. )
  1015. },
  1016. E2E_TIMEOUT,
  1017. )
  1018. test(
  1019. 'custom transition higher-order component',
  1020. async () => {
  1021. await page().evaluate(() => {
  1022. const { createApp, ref, h, Transition } = (window as any).Vue
  1023. createApp({
  1024. template: `
  1025. <div id="container"><my-transition><div v-if="toggle" class="test">content</div></my-transition></div>
  1026. <button id="toggleBtn" @click="click">button</button>
  1027. `,
  1028. components: {
  1029. 'my-transition': (props: any, { slots }: any) => {
  1030. return h(Transition, { name: 'test' }, slots)
  1031. },
  1032. },
  1033. setup: () => {
  1034. const toggle = ref(true)
  1035. const click = () => (toggle.value = !toggle.value)
  1036. return { toggle, click }
  1037. },
  1038. }).mount('#app')
  1039. })
  1040. expect(await html('#container')).toBe('<div class="test">content</div>')
  1041. // leave
  1042. expect(await classWhenTransitionStart()).toStrictEqual([
  1043. 'test',
  1044. 'test-leave-from',
  1045. 'test-leave-active',
  1046. ])
  1047. await nextFrame()
  1048. expect(await classList('.test')).toStrictEqual([
  1049. 'test',
  1050. 'test-leave-active',
  1051. 'test-leave-to',
  1052. ])
  1053. await transitionFinish()
  1054. expect(await html('#container')).toBe('<!--v-if-->')
  1055. // enter
  1056. expect(await classWhenTransitionStart()).toStrictEqual([
  1057. 'test',
  1058. 'test-enter-from',
  1059. 'test-enter-active',
  1060. ])
  1061. await nextFrame()
  1062. expect(await classList('.test')).toStrictEqual([
  1063. 'test',
  1064. 'test-enter-active',
  1065. 'test-enter-to',
  1066. ])
  1067. await transitionFinish()
  1068. expect(await html('#container')).toBe('<div class="test">content</div>')
  1069. },
  1070. E2E_TIMEOUT,
  1071. )
  1072. test(
  1073. 'transition on child components with empty root node',
  1074. async () => {
  1075. await page().evaluate(() => {
  1076. const { createApp, ref } = (window as any).Vue
  1077. createApp({
  1078. template: `
  1079. <div id="container">
  1080. <transition name="test">
  1081. <component class="test" :is="view"></component>
  1082. </transition>
  1083. </div>
  1084. <button id="toggleBtn" @click="click">button</button>
  1085. <button id="changeViewBtn" @click="change">button</button>
  1086. `,
  1087. components: {
  1088. one: {
  1089. template: '<div v-if="false">one</div>',
  1090. },
  1091. two: {
  1092. template: '<div>two</div>',
  1093. },
  1094. },
  1095. setup: () => {
  1096. const toggle = ref(true)
  1097. const view = ref('one')
  1098. const click = () => (toggle.value = !toggle.value)
  1099. const change = () =>
  1100. (view.value = view.value === 'one' ? 'two' : 'one')
  1101. return { toggle, click, change, view }
  1102. },
  1103. }).mount('#app')
  1104. })
  1105. expect(await html('#container')).toBe('<!--v-if-->')
  1106. // change view -> 'two'
  1107. await page().evaluate(() => {
  1108. ;(document.querySelector('#changeViewBtn') as any)!.click()
  1109. })
  1110. // enter
  1111. expect(await classWhenTransitionStart()).toStrictEqual([
  1112. 'test',
  1113. 'test-enter-from',
  1114. 'test-enter-active',
  1115. ])
  1116. await nextFrame()
  1117. expect(await classList('.test')).toStrictEqual([
  1118. 'test',
  1119. 'test-enter-active',
  1120. 'test-enter-to',
  1121. ])
  1122. await transitionFinish()
  1123. expect(await html('#container')).toBe('<div class="test">two</div>')
  1124. // change view -> 'one'
  1125. await page().evaluate(() => {
  1126. ;(document.querySelector('#changeViewBtn') as any)!.click()
  1127. })
  1128. // leave
  1129. expect(await classWhenTransitionStart()).toStrictEqual([
  1130. 'test',
  1131. 'test-leave-from',
  1132. 'test-leave-active',
  1133. ])
  1134. await nextFrame()
  1135. expect(await classList('.test')).toStrictEqual([
  1136. 'test',
  1137. 'test-leave-active',
  1138. 'test-leave-to',
  1139. ])
  1140. await transitionFinish()
  1141. expect(await html('#container')).toBe('<!--v-if-->')
  1142. },
  1143. E2E_TIMEOUT,
  1144. )
  1145. // issue https://github.com/vuejs/core/issues/7649
  1146. test(
  1147. 'transition with v-if at component root-level',
  1148. async () => {
  1149. await page().evaluate(() => {
  1150. const { createApp, ref } = (window as any).Vue
  1151. createApp({
  1152. template: `
  1153. <div id="container">
  1154. <transition name="test" mode="out-in">
  1155. <component class="test" :is="view"></component>
  1156. </transition>
  1157. </div>
  1158. <button id="toggleBtn" @click="click">button</button>
  1159. <button id="changeViewBtn" @click="change">button</button>
  1160. `,
  1161. components: {
  1162. one: {
  1163. template: '<div v-if="false">one</div>',
  1164. },
  1165. two: {
  1166. template: '<div>two</div>',
  1167. },
  1168. },
  1169. setup: () => {
  1170. const toggle = ref(true)
  1171. const view = ref('one')
  1172. const click = () => (toggle.value = !toggle.value)
  1173. const change = () =>
  1174. (view.value = view.value === 'one' ? 'two' : 'one')
  1175. return { toggle, click, change, view }
  1176. },
  1177. }).mount('#app')
  1178. })
  1179. expect(await html('#container')).toBe('<!--v-if-->')
  1180. // change view -> 'two'
  1181. await page().evaluate(() => {
  1182. ;(document.querySelector('#changeViewBtn') as any)!.click()
  1183. })
  1184. // enter
  1185. expect(await classWhenTransitionStart()).toStrictEqual([
  1186. 'test',
  1187. 'test-enter-from',
  1188. 'test-enter-active',
  1189. ])
  1190. await nextFrame()
  1191. expect(await classList('.test')).toStrictEqual([
  1192. 'test',
  1193. 'test-enter-active',
  1194. 'test-enter-to',
  1195. ])
  1196. await transitionFinish()
  1197. expect(await html('#container')).toBe('<div class="test">two</div>')
  1198. // change view -> 'one'
  1199. await page().evaluate(() => {
  1200. ;(document.querySelector('#changeViewBtn') as any)!.click()
  1201. })
  1202. // leave
  1203. expect(await classWhenTransitionStart()).toStrictEqual([
  1204. 'test',
  1205. 'test-leave-from',
  1206. 'test-leave-active',
  1207. ])
  1208. await nextFrame()
  1209. expect(await classList('.test')).toStrictEqual([
  1210. 'test',
  1211. 'test-leave-active',
  1212. 'test-leave-to',
  1213. ])
  1214. await transitionFinish()
  1215. expect(await html('#container')).toBe('<!--v-if-->')
  1216. },
  1217. E2E_TIMEOUT,
  1218. )
  1219. // #3716
  1220. test(
  1221. 'wrapping transition + fallthrough attrs',
  1222. async () => {
  1223. await page().evaluate(() => {
  1224. const { createApp, ref } = (window as any).Vue
  1225. createApp({
  1226. components: {
  1227. 'my-transition': {
  1228. template: `
  1229. <transition foo="1" name="test">
  1230. <slot></slot>
  1231. </transition>
  1232. `,
  1233. },
  1234. },
  1235. template: `
  1236. <div id="container">
  1237. <my-transition>
  1238. <div v-if="toggle">content</div>
  1239. </my-transition>
  1240. </div>
  1241. <button id="toggleBtn" @click="click">button</button>
  1242. `,
  1243. setup: () => {
  1244. const toggle = ref(true)
  1245. const click = () => (toggle.value = !toggle.value)
  1246. return { toggle, click }
  1247. },
  1248. }).mount('#app')
  1249. })
  1250. expect(await html('#container')).toBe('<div foo="1">content</div>')
  1251. await click('#toggleBtn')
  1252. // toggle again before leave finishes
  1253. await nextTick()
  1254. await click('#toggleBtn')
  1255. await transitionFinish()
  1256. expect(await html('#container')).toBe(
  1257. '<div foo="1" class="">content</div>',
  1258. )
  1259. },
  1260. E2E_TIMEOUT,
  1261. )
  1262. // #11061
  1263. test(
  1264. 'transition + fallthrough attrs (in-out mode)',
  1265. async () => {
  1266. const beforeLeaveSpy = vi.fn()
  1267. const onLeaveSpy = vi.fn()
  1268. const afterLeaveSpy = vi.fn()
  1269. const beforeEnterSpy = vi.fn()
  1270. const onEnterSpy = vi.fn()
  1271. const afterEnterSpy = vi.fn()
  1272. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  1273. await page().exposeFunction('onEnterSpy', onEnterSpy)
  1274. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  1275. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  1276. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  1277. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  1278. await page().evaluate(() => {
  1279. const { onEnterSpy, onLeaveSpy } = window as any
  1280. const { createApp, ref } = (window as any).Vue
  1281. createApp({
  1282. components: {
  1283. one: {
  1284. template: '<div>one</div>',
  1285. },
  1286. two: {
  1287. template: '<div>two</div>',
  1288. },
  1289. },
  1290. template: `
  1291. <div id="container">
  1292. <transition foo="1" name="test" mode="in-out"
  1293. @before-enter="beforeEnterSpy()"
  1294. @enter="onEnterSpy()"
  1295. @after-enter="afterEnterSpy()"
  1296. @before-leave="beforeLeaveSpy()"
  1297. @leave="onLeaveSpy()"
  1298. @after-leave="afterLeaveSpy()">
  1299. <component :is="view"></component>
  1300. </transition>
  1301. </div>
  1302. <button id="toggleBtn" @click="click">button</button>
  1303. `,
  1304. setup: () => {
  1305. const view = ref('one')
  1306. const click = () =>
  1307. (view.value = view.value === 'one' ? 'two' : 'one')
  1308. return {
  1309. view,
  1310. click,
  1311. beforeEnterSpy,
  1312. onEnterSpy,
  1313. afterEnterSpy,
  1314. beforeLeaveSpy,
  1315. onLeaveSpy,
  1316. afterLeaveSpy,
  1317. }
  1318. },
  1319. }).mount('#app')
  1320. })
  1321. expect(await html('#container')).toBe('<div foo="1">one</div>')
  1322. // toggle
  1323. await click('#toggleBtn')
  1324. await nextTick()
  1325. await transitionFinish()
  1326. expect(beforeEnterSpy).toBeCalledTimes(1)
  1327. expect(onEnterSpy).toBeCalledTimes(1)
  1328. expect(afterEnterSpy).toBeCalledTimes(1)
  1329. expect(beforeLeaveSpy).toBeCalledTimes(1)
  1330. expect(onLeaveSpy).toBeCalledTimes(1)
  1331. expect(afterLeaveSpy).toBeCalledTimes(1)
  1332. expect(await html('#container')).toBe('<div foo="1" class="">two</div>')
  1333. // toggle back
  1334. await click('#toggleBtn')
  1335. await nextTick()
  1336. await transitionFinish()
  1337. expect(beforeEnterSpy).toBeCalledTimes(2)
  1338. expect(onEnterSpy).toBeCalledTimes(2)
  1339. expect(afterEnterSpy).toBeCalledTimes(2)
  1340. expect(beforeLeaveSpy).toBeCalledTimes(2)
  1341. expect(onLeaveSpy).toBeCalledTimes(2)
  1342. expect(afterLeaveSpy).toBeCalledTimes(2)
  1343. expect(await html('#container')).toBe('<div foo="1" class="">one</div>')
  1344. },
  1345. E2E_TIMEOUT,
  1346. )
  1347. })
  1348. describe('transition with KeepAlive', () => {
  1349. test(
  1350. 'unmount innerChild (out-in mode)',
  1351. async () => {
  1352. const unmountSpy = vi.fn()
  1353. await page().exposeFunction('unmountSpy', unmountSpy)
  1354. await page().evaluate(() => {
  1355. const { unmountSpy } = window as any
  1356. const { createApp, ref, h, onUnmounted } = (window as any).Vue
  1357. createApp({
  1358. template: `
  1359. <div id="container">
  1360. <transition mode="out-in">
  1361. <KeepAlive :include="includeRef">
  1362. <TrueBranch v-if="toggle"></TrueBranch>
  1363. </KeepAlive>
  1364. </transition>
  1365. </div>
  1366. <button id="toggleBtn" @click="click">button</button>
  1367. `,
  1368. components: {
  1369. TrueBranch: {
  1370. name: 'TrueBranch',
  1371. setup() {
  1372. onUnmounted(unmountSpy)
  1373. const count = ref(0)
  1374. return () => h('div', count.value)
  1375. },
  1376. },
  1377. },
  1378. setup: () => {
  1379. const includeRef = ref(['TrueBranch'])
  1380. const toggle = ref(true)
  1381. const click = () => {
  1382. toggle.value = !toggle.value
  1383. if (toggle.value) {
  1384. includeRef.value = ['TrueBranch']
  1385. } else {
  1386. includeRef.value = []
  1387. }
  1388. }
  1389. return { toggle, click, unmountSpy, includeRef }
  1390. },
  1391. }).mount('#app')
  1392. })
  1393. await transitionFinish()
  1394. expect(await html('#container')).toBe('<div>0</div>')
  1395. await click('#toggleBtn')
  1396. await transitionFinish()
  1397. expect(await html('#container')).toBe('<!--v-if-->')
  1398. expect(unmountSpy).toBeCalledTimes(1)
  1399. },
  1400. E2E_TIMEOUT,
  1401. )
  1402. // #11775
  1403. test(
  1404. 'switch child then update include (out-in mode)',
  1405. async () => {
  1406. const onUpdatedSpyA = vi.fn()
  1407. const onUnmountedSpyC = vi.fn()
  1408. await page().exposeFunction('onUpdatedSpyA', onUpdatedSpyA)
  1409. await page().exposeFunction('onUnmountedSpyC', onUnmountedSpyC)
  1410. await page().evaluate(() => {
  1411. const { onUpdatedSpyA, onUnmountedSpyC } = window as any
  1412. const { createApp, ref, shallowRef, h, onUpdated, onUnmounted } = (
  1413. window as any
  1414. ).Vue
  1415. createApp({
  1416. template: `
  1417. <div id="container">
  1418. <transition mode="out-in">
  1419. <KeepAlive :include="includeRef">
  1420. <component :is="current" />
  1421. </KeepAlive>
  1422. </transition>
  1423. </div>
  1424. <button id="switchToB" @click="switchToB">switchToB</button>
  1425. <button id="switchToC" @click="switchToC">switchToC</button>
  1426. <button id="switchToA" @click="switchToA">switchToA</button>
  1427. `,
  1428. components: {
  1429. CompA: {
  1430. name: 'CompA',
  1431. setup() {
  1432. onUpdated(onUpdatedSpyA)
  1433. return () => h('div', 'CompA')
  1434. },
  1435. },
  1436. CompB: {
  1437. name: 'CompB',
  1438. setup() {
  1439. return () => h('div', 'CompB')
  1440. },
  1441. },
  1442. CompC: {
  1443. name: 'CompC',
  1444. setup() {
  1445. onUnmounted(onUnmountedSpyC)
  1446. return () => h('div', 'CompC')
  1447. },
  1448. },
  1449. },
  1450. setup: () => {
  1451. const includeRef = ref(['CompA', 'CompB', 'CompC'])
  1452. const current = shallowRef('CompA')
  1453. const switchToB = () => (current.value = 'CompB')
  1454. const switchToC = () => (current.value = 'CompC')
  1455. const switchToA = () => {
  1456. current.value = 'CompA'
  1457. includeRef.value = ['CompA']
  1458. }
  1459. return { current, switchToB, switchToC, switchToA, includeRef }
  1460. },
  1461. }).mount('#app')
  1462. })
  1463. await transitionFinish()
  1464. expect(await html('#container')).toBe('<div>CompA</div>')
  1465. await click('#switchToB')
  1466. await nextTick()
  1467. await click('#switchToC')
  1468. await transitionFinish()
  1469. expect(await html('#container')).toBe('<div class="">CompC</div>')
  1470. await click('#switchToA')
  1471. await transitionFinish()
  1472. expect(await html('#container')).toBe('<div class="">CompA</div>')
  1473. // expect CompA only update once
  1474. expect(onUpdatedSpyA).toBeCalledTimes(1)
  1475. expect(onUnmountedSpyC).toBeCalledTimes(1)
  1476. },
  1477. E2E_TIMEOUT,
  1478. )
  1479. // #10827
  1480. test(
  1481. 'switch and update child then update include (out-in mode)',
  1482. async () => {
  1483. const onUnmountedSpyB = vi.fn()
  1484. await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB)
  1485. await page().evaluate(() => {
  1486. const { onUnmountedSpyB } = window as any
  1487. const {
  1488. createApp,
  1489. ref,
  1490. shallowRef,
  1491. h,
  1492. provide,
  1493. inject,
  1494. onUnmounted,
  1495. } = (window as any).Vue
  1496. createApp({
  1497. template: `
  1498. <div id="container">
  1499. <transition name="test-anim" mode="out-in">
  1500. <KeepAlive :include="includeRef">
  1501. <component :is="current" />
  1502. </KeepAlive>
  1503. </transition>
  1504. </div>
  1505. <button id="switchToA" @click="switchToA">switchToA</button>
  1506. <button id="switchToB" @click="switchToB">switchToB</button>
  1507. `,
  1508. components: {
  1509. CompA: {
  1510. name: 'CompA',
  1511. setup() {
  1512. const current = inject('current')
  1513. return () => h('div', current.value)
  1514. },
  1515. },
  1516. CompB: {
  1517. name: 'CompB',
  1518. setup() {
  1519. const current = inject('current')
  1520. onUnmounted(onUnmountedSpyB)
  1521. return () => h('div', current.value)
  1522. },
  1523. },
  1524. },
  1525. setup: () => {
  1526. const includeRef = ref(['CompA'])
  1527. const current = shallowRef('CompA')
  1528. provide('current', current)
  1529. const switchToB = () => {
  1530. current.value = 'CompB'
  1531. includeRef.value = ['CompA', 'CompB']
  1532. }
  1533. const switchToA = () => {
  1534. current.value = 'CompA'
  1535. includeRef.value = ['CompA']
  1536. }
  1537. return { current, switchToB, switchToA, includeRef }
  1538. },
  1539. }).mount('#app')
  1540. })
  1541. await transitionFinish()
  1542. expect(await html('#container')).toBe('<div>CompA</div>')
  1543. await click('#switchToB')
  1544. await transitionFinish()
  1545. await transitionFinish()
  1546. expect(await html('#container')).toBe('<div class="">CompB</div>')
  1547. await click('#switchToA')
  1548. await transitionFinish()
  1549. await transitionFinish()
  1550. expect(await html('#container')).toBe('<div class="">CompA</div>')
  1551. expect(onUnmountedSpyB).toBeCalledTimes(1)
  1552. },
  1553. E2E_TIMEOUT,
  1554. )
  1555. })
  1556. describe('transition with Suspense', () => {
  1557. // #1583
  1558. test(
  1559. 'async component transition inside Suspense',
  1560. async () => {
  1561. const onLeaveSpy = vi.fn()
  1562. const onEnterSpy = vi.fn()
  1563. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  1564. await page().exposeFunction('onEnterSpy', onEnterSpy)
  1565. await page().evaluate(() => {
  1566. const { onEnterSpy, onLeaveSpy } = window as any
  1567. const { createApp, ref, h } = (window as any).Vue
  1568. createApp({
  1569. template: `
  1570. <div id="container">
  1571. <transition @enter="onEnterSpy()" @leave="onLeaveSpy()">
  1572. <Suspense>
  1573. <Comp v-if="toggle" class="test">content</Comp>
  1574. </Suspense>
  1575. </transition>
  1576. </div>
  1577. <button id="toggleBtn" @click="click">button</button>
  1578. `,
  1579. components: {
  1580. Comp: {
  1581. async setup() {
  1582. return () => h('div', { class: 'test' }, 'content')
  1583. },
  1584. },
  1585. },
  1586. setup: () => {
  1587. const toggle = ref(true)
  1588. const click = () => (toggle.value = !toggle.value)
  1589. return { toggle, click, onEnterSpy, onLeaveSpy }
  1590. },
  1591. }).mount('#app')
  1592. })
  1593. expect(onEnterSpy).toBeCalledTimes(1)
  1594. await nextFrame()
  1595. expect(await html('#container')).toBe(
  1596. '<div class="test v-enter-active v-enter-to">content</div>',
  1597. )
  1598. await transitionFinish()
  1599. expect(await html('#container')).toBe('<div class="test">content</div>')
  1600. // leave
  1601. expect(await classWhenTransitionStart()).toStrictEqual([
  1602. 'test',
  1603. 'v-leave-from',
  1604. 'v-leave-active',
  1605. ])
  1606. expect(onLeaveSpy).toBeCalledTimes(1)
  1607. await nextFrame()
  1608. expect(await classList('.test')).toStrictEqual([
  1609. 'test',
  1610. 'v-leave-active',
  1611. 'v-leave-to',
  1612. ])
  1613. await transitionFinish()
  1614. expect(await html('#container')).toBe('<!--v-if-->')
  1615. // enter
  1616. const enterClass = await page().evaluate(async () => {
  1617. ;(document.querySelector('#toggleBtn') as any)!.click()
  1618. // nextTrick for patch start
  1619. await Promise.resolve()
  1620. // nextTrick for Suspense resolve
  1621. await Promise.resolve()
  1622. // nextTrick for dom transition start
  1623. await Promise.resolve()
  1624. return document
  1625. .querySelector('#container div')!
  1626. .className.split(/\s+/g)
  1627. })
  1628. expect(enterClass).toStrictEqual([
  1629. 'test',
  1630. 'v-enter-from',
  1631. 'v-enter-active',
  1632. ])
  1633. expect(onEnterSpy).toBeCalledTimes(2)
  1634. await nextFrame()
  1635. expect(await classList('.test')).toStrictEqual([
  1636. 'test',
  1637. 'v-enter-active',
  1638. 'v-enter-to',
  1639. ])
  1640. await transitionFinish()
  1641. expect(await html('#container')).toBe('<div class="test">content</div>')
  1642. },
  1643. E2E_TIMEOUT,
  1644. )
  1645. // #1689
  1646. test(
  1647. 'static node transition inside Suspense',
  1648. async () => {
  1649. await page().evaluate(() => {
  1650. const { createApp, ref } = (window as any).Vue
  1651. createApp({
  1652. template: `
  1653. <div id="container">
  1654. <transition>
  1655. <Suspense>
  1656. <div v-if="toggle" class="test">content</div>
  1657. </Suspense>
  1658. </transition>
  1659. </div>
  1660. <button id="toggleBtn" @click="click">button</button>
  1661. `,
  1662. setup: () => {
  1663. const toggle = ref(true)
  1664. const click = () => (toggle.value = !toggle.value)
  1665. return { toggle, click }
  1666. },
  1667. }).mount('#app')
  1668. })
  1669. expect(await html('#container')).toBe('<div class="test">content</div>')
  1670. // leave
  1671. expect(await classWhenTransitionStart()).toStrictEqual([
  1672. 'test',
  1673. 'v-leave-from',
  1674. 'v-leave-active',
  1675. ])
  1676. await nextFrame()
  1677. expect(await classList('.test')).toStrictEqual([
  1678. 'test',
  1679. 'v-leave-active',
  1680. 'v-leave-to',
  1681. ])
  1682. await transitionFinish()
  1683. expect(await html('#container')).toBe('<!--v-if-->')
  1684. // enter
  1685. expect(await classWhenTransitionStart()).toStrictEqual([
  1686. 'test',
  1687. 'v-enter-from',
  1688. 'v-enter-active',
  1689. ])
  1690. await nextFrame()
  1691. expect(await classList('.test')).toStrictEqual([
  1692. 'test',
  1693. 'v-enter-active',
  1694. 'v-enter-to',
  1695. ])
  1696. await transitionFinish()
  1697. expect(await html('#container')).toBe('<div class="test">content</div>')
  1698. },
  1699. E2E_TIMEOUT,
  1700. )
  1701. test(
  1702. 'out-in mode with Suspense',
  1703. async () => {
  1704. const onLeaveSpy = vi.fn()
  1705. const onEnterSpy = vi.fn()
  1706. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  1707. await page().exposeFunction('onEnterSpy', onEnterSpy)
  1708. await page().evaluate(() => {
  1709. const { createApp, shallowRef, h } = (window as any).Vue
  1710. const One = {
  1711. async setup() {
  1712. return () => h('div', { class: 'test' }, 'one')
  1713. },
  1714. }
  1715. const Two = {
  1716. async setup() {
  1717. return () => h('div', { class: 'test' }, 'two')
  1718. },
  1719. }
  1720. createApp({
  1721. template: `
  1722. <div id="container">
  1723. <transition mode="out-in">
  1724. <Suspense>
  1725. <component :is="view"/>
  1726. </Suspense>
  1727. </transition>
  1728. </div>
  1729. <button id="toggleBtn" @click="click">button</button>
  1730. `,
  1731. setup: () => {
  1732. const view = shallowRef(One)
  1733. const click = () => {
  1734. view.value = view.value === One ? Two : One
  1735. }
  1736. return { view, click }
  1737. },
  1738. }).mount('#app')
  1739. })
  1740. await nextFrame()
  1741. expect(await html('#container')).toBe(
  1742. '<div class="test v-enter-active v-enter-to">one</div>',
  1743. )
  1744. await transitionFinish()
  1745. expect(await html('#container')).toBe('<div class="test">one</div>')
  1746. // leave
  1747. await classWhenTransitionStart()
  1748. await nextFrame()
  1749. expect(await html('#container')).toBe(
  1750. '<div class="test v-leave-active v-leave-to">one</div>',
  1751. )
  1752. await transitionFinish()
  1753. await nextFrame()
  1754. // expect(await html('#container')).toBe(
  1755. // '<div class="test v-enter-active v-enter-to">two</div>'
  1756. // )
  1757. await transitionFinish()
  1758. expect(await html('#container')).toBe('<div class="test">two</div>')
  1759. },
  1760. E2E_TIMEOUT,
  1761. )
  1762. // #3963
  1763. test(
  1764. 'Suspense fallback should work with transition',
  1765. async () => {
  1766. await page().evaluate(() => {
  1767. const { createApp, shallowRef, h } = (window as any).Vue
  1768. const One = {
  1769. template: `<div>{{ msg }}</div>`,
  1770. setup() {
  1771. return new Promise(_resolve => {
  1772. // @ts-expect-error
  1773. window.resolve = () =>
  1774. _resolve({
  1775. msg: 'success',
  1776. })
  1777. })
  1778. },
  1779. }
  1780. createApp({
  1781. template: `
  1782. <div id="container">
  1783. <transition mode="out-in">
  1784. <Suspense :timeout="0">
  1785. <template #default>
  1786. <component :is="view" />
  1787. </template>
  1788. <template #fallback>
  1789. <div>Loading...</div>
  1790. </template>
  1791. </Suspense>
  1792. </transition>
  1793. </div>
  1794. <button id="toggleBtn" @click="click">button</button>
  1795. `,
  1796. setup: () => {
  1797. const view = shallowRef(null)
  1798. const click = () => {
  1799. view.value = view.value ? null : h(One)
  1800. }
  1801. return { view, click }
  1802. },
  1803. }).mount('#app')
  1804. })
  1805. expect(await html('#container')).toBe('<!---->')
  1806. await click('#toggleBtn')
  1807. await nextFrame()
  1808. expect(await html('#container')).toBe('<div class="">Loading...</div>')
  1809. await page().evaluate(() => {
  1810. // @ts-expect-error
  1811. window.resolve()
  1812. })
  1813. await transitionFinish(duration * 2)
  1814. expect(await html('#container')).toBe('<div class="">success</div>')
  1815. },
  1816. E2E_TIMEOUT,
  1817. )
  1818. // #5844
  1819. test('children mount should be called after html changes', async () => {
  1820. const fooMountSpy = vi.fn()
  1821. const barMountSpy = vi.fn()
  1822. await page().exposeFunction('fooMountSpy', fooMountSpy)
  1823. await page().exposeFunction('barMountSpy', barMountSpy)
  1824. await page().evaluate(() => {
  1825. const { fooMountSpy, barMountSpy } = window as any
  1826. const { createApp, ref, h, onMounted } = (window as any).Vue
  1827. createApp({
  1828. template: `
  1829. <div id="container">
  1830. <transition mode="out-in">
  1831. <Suspense>
  1832. <Foo v-if="toggle" />
  1833. <Bar v-else />
  1834. </Suspense>
  1835. </transition>
  1836. </div>
  1837. <button id="toggleBtn" @click="click">button</button>
  1838. `,
  1839. components: {
  1840. Foo: {
  1841. setup() {
  1842. const el = ref(null)
  1843. onMounted(() => {
  1844. fooMountSpy(
  1845. !!el.value,
  1846. !!document.getElementById('foo'),
  1847. !!document.getElementById('bar'),
  1848. )
  1849. })
  1850. return () => h('div', { ref: el, id: 'foo' }, 'Foo')
  1851. },
  1852. },
  1853. Bar: {
  1854. setup() {
  1855. const el = ref(null)
  1856. onMounted(() => {
  1857. barMountSpy(
  1858. !!el.value,
  1859. !!document.getElementById('foo'),
  1860. !!document.getElementById('bar'),
  1861. )
  1862. })
  1863. return () => h('div', { ref: el, id: 'bar' }, 'Bar')
  1864. },
  1865. },
  1866. },
  1867. setup: () => {
  1868. const toggle = ref(true)
  1869. const click = () => (toggle.value = !toggle.value)
  1870. return { toggle, click }
  1871. },
  1872. }).mount('#app')
  1873. })
  1874. await nextFrame()
  1875. expect(await html('#container')).toBe('<div id="foo">Foo</div>')
  1876. await transitionFinish()
  1877. expect(fooMountSpy).toBeCalledTimes(1)
  1878. expect(fooMountSpy).toHaveBeenNthCalledWith(1, true, true, false)
  1879. await page().evaluate(async () => {
  1880. ;(document.querySelector('#toggleBtn') as any)!.click()
  1881. // nextTrick for patch start
  1882. await Promise.resolve()
  1883. // nextTrick for Suspense resolve
  1884. await Promise.resolve()
  1885. // nextTrick for dom transition start
  1886. await Promise.resolve()
  1887. return document.querySelector('#container div')!.className.split(/\s+/g)
  1888. })
  1889. await nextFrame()
  1890. await transitionFinish()
  1891. expect(await html('#container')).toBe('<div id="bar" class="">Bar</div>')
  1892. expect(barMountSpy).toBeCalledTimes(1)
  1893. expect(barMountSpy).toHaveBeenNthCalledWith(1, true, false, true)
  1894. })
  1895. // #8105
  1896. test(
  1897. 'trigger again when transition is not finished',
  1898. async () => {
  1899. await page().evaluate(duration => {
  1900. const { createApp, shallowRef, h } = (window as any).Vue
  1901. const One = {
  1902. async setup() {
  1903. return () => h('div', { class: 'test' }, 'one')
  1904. },
  1905. }
  1906. const Two = {
  1907. async setup() {
  1908. return () => h('div', { class: 'test' }, 'two')
  1909. },
  1910. }
  1911. createApp({
  1912. template: `
  1913. <div id="container">
  1914. <transition name="test" mode="out-in" duration="${duration}">
  1915. <Suspense>
  1916. <component :is="view"/>
  1917. </Suspense>
  1918. </transition>
  1919. </div>
  1920. <button id="toggleBtn" @click="click">button</button>
  1921. `,
  1922. setup: () => {
  1923. const view = shallowRef(One)
  1924. const click = () => {
  1925. view.value = view.value === One ? Two : One
  1926. }
  1927. return { view, click }
  1928. },
  1929. }).mount('#app')
  1930. }, duration)
  1931. await nextFrame()
  1932. expect(await html('#container')).toBe(
  1933. '<div class="test test-enter-active test-enter-to">one</div>',
  1934. )
  1935. await transitionFinish()
  1936. expect(await html('#container')).toBe('<div class="test">one</div>')
  1937. // trigger twice
  1938. classWhenTransitionStart()
  1939. classWhenTransitionStart()
  1940. await nextFrame()
  1941. expect(await html('#container')).toBe(
  1942. '<div class="test test-leave-active test-leave-to">one</div>',
  1943. )
  1944. await transitionFinish()
  1945. await nextFrame()
  1946. expect(await html('#container')).toBe(
  1947. '<div class="test test-enter-active test-enter-to">one</div>',
  1948. )
  1949. await transitionFinish()
  1950. await nextFrame()
  1951. expect(await html('#container')).toBe('<div class="test">one</div>')
  1952. },
  1953. E2E_TIMEOUT,
  1954. )
  1955. // #9996
  1956. test(
  1957. 'trigger again when transition is not finished & correctly anchor',
  1958. async () => {
  1959. await page().evaluate(duration => {
  1960. const { createApp, shallowRef, h } = (window as any).Vue
  1961. const One = {
  1962. async setup() {
  1963. return () => h('div', { class: 'test' }, 'one')
  1964. },
  1965. }
  1966. const Two = {
  1967. async setup() {
  1968. return () => h('div', { class: 'test' }, 'two')
  1969. },
  1970. }
  1971. createApp({
  1972. template: `
  1973. <div id="container">
  1974. <div>Top</div>
  1975. <transition name="test" mode="out-in" :duration="${duration}">
  1976. <Suspense>
  1977. <component :is="view"/>
  1978. </Suspense>
  1979. </transition>
  1980. <div>Bottom</div>
  1981. </div>
  1982. <button id="toggleBtn" @click="click">button</button>
  1983. `,
  1984. setup: () => {
  1985. const view = shallowRef(One)
  1986. const click = () => {
  1987. view.value = view.value === One ? Two : One
  1988. }
  1989. return { view, click }
  1990. },
  1991. }).mount('#app')
  1992. }, duration)
  1993. await nextFrame()
  1994. expect(await html('#container')).toBe(
  1995. '<div>Top</div><div class="test test-enter-active test-enter-to">one</div><div>Bottom</div>',
  1996. )
  1997. await transitionFinish()
  1998. expect(await html('#container')).toBe(
  1999. '<div>Top</div><div class="test">one</div><div>Bottom</div>',
  2000. )
  2001. // trigger twice
  2002. classWhenTransitionStart()
  2003. await nextFrame()
  2004. expect(await html('#container')).toBe(
  2005. '<div>Top</div><div class="test test-leave-active test-leave-to">one</div><div>Bottom</div>',
  2006. )
  2007. await transitionFinish()
  2008. await nextFrame()
  2009. expect(await html('#container')).toBe(
  2010. '<div>Top</div><div class="test test-enter-active test-enter-to">two</div><div>Bottom</div>',
  2011. )
  2012. await transitionFinish()
  2013. await nextFrame()
  2014. expect(await html('#container')).toBe(
  2015. '<div>Top</div><div class="test">two</div><div>Bottom</div>',
  2016. )
  2017. },
  2018. E2E_TIMEOUT,
  2019. )
  2020. // #11806
  2021. test(
  2022. 'switch between Async and Sync child when transition is not finished',
  2023. async () => {
  2024. await page().evaluate(() => {
  2025. const { createApp, shallowRef, h, nextTick } = (window as any).Vue
  2026. createApp({
  2027. template: `
  2028. <div id="container">
  2029. <Transition mode="out-in">
  2030. <Suspense>
  2031. <component :is="view"/>
  2032. </Suspense>
  2033. </Transition>
  2034. </div>
  2035. <button id="toggleBtn" @click="click">button</button>
  2036. `,
  2037. setup: () => {
  2038. const view = shallowRef('SyncB')
  2039. const click = async () => {
  2040. view.value = 'SyncA'
  2041. await nextTick()
  2042. view.value = 'AsyncB'
  2043. await nextTick()
  2044. view.value = 'SyncB'
  2045. }
  2046. return { view, click }
  2047. },
  2048. components: {
  2049. SyncA: {
  2050. setup() {
  2051. return () => h('div', 'SyncA')
  2052. },
  2053. },
  2054. AsyncB: {
  2055. async setup() {
  2056. await nextTick()
  2057. return () => h('div', 'AsyncB')
  2058. },
  2059. },
  2060. SyncB: {
  2061. setup() {
  2062. return () => h('div', 'SyncB')
  2063. },
  2064. },
  2065. },
  2066. }).mount('#app')
  2067. })
  2068. expect(await html('#container')).toBe('<div>SyncB</div>')
  2069. await click('#toggleBtn')
  2070. await nextFrame()
  2071. await transitionFinish()
  2072. await transitionFinish()
  2073. expect(await html('#container')).toBe('<div class="">SyncB</div>')
  2074. },
  2075. E2E_TIMEOUT,
  2076. )
  2077. })
  2078. describe('transition with Teleport', () => {
  2079. test(
  2080. 'apply transition to teleport child',
  2081. async () => {
  2082. await page().evaluate(() => {
  2083. const { createApp, ref, h } = (window as any).Vue
  2084. createApp({
  2085. template: `
  2086. <div id="target"></div>
  2087. <div id="container">
  2088. <transition>
  2089. <Teleport to="#target">
  2090. <!-- comment -->
  2091. <Comp v-if="toggle" class="test">content</Comp>
  2092. </Teleport>
  2093. </transition>
  2094. </div>
  2095. <button id="toggleBtn" @click="click">button</button>
  2096. `,
  2097. components: {
  2098. Comp: {
  2099. setup() {
  2100. return () => h('div', { class: 'test' }, 'content')
  2101. },
  2102. },
  2103. },
  2104. setup: () => {
  2105. const toggle = ref(false)
  2106. const click = () => (toggle.value = !toggle.value)
  2107. return { toggle, click }
  2108. },
  2109. }).mount('#app')
  2110. })
  2111. expect(await html('#target')).toBe('<!-- comment --><!--v-if-->')
  2112. expect(await html('#container')).toBe(
  2113. '<!--teleport start--><!--teleport end-->',
  2114. )
  2115. const classWhenTransitionStart = () =>
  2116. page().evaluate(() => {
  2117. ;(document.querySelector('#toggleBtn') as any)!.click()
  2118. return Promise.resolve().then(() => {
  2119. // find the class of teleported node
  2120. return document
  2121. .querySelector('#target div')!
  2122. .className.split(/\s+/g)
  2123. })
  2124. })
  2125. // enter
  2126. expect(await classWhenTransitionStart()).toStrictEqual([
  2127. 'test',
  2128. 'v-enter-from',
  2129. 'v-enter-active',
  2130. ])
  2131. await nextFrame()
  2132. expect(await classList('.test')).toStrictEqual([
  2133. 'test',
  2134. 'v-enter-active',
  2135. 'v-enter-to',
  2136. ])
  2137. await transitionFinish()
  2138. expect(await html('#target')).toBe(
  2139. '<!-- comment --><div class="test">content</div>',
  2140. )
  2141. // leave
  2142. expect(await classWhenTransitionStart()).toStrictEqual([
  2143. 'test',
  2144. 'v-leave-from',
  2145. 'v-leave-active',
  2146. ])
  2147. await nextFrame()
  2148. expect(await classList('.test')).toStrictEqual([
  2149. 'test',
  2150. 'v-leave-active',
  2151. 'v-leave-to',
  2152. ])
  2153. await transitionFinish()
  2154. expect(await html('#target')).toBe('<!-- comment --><!--v-if-->')
  2155. expect(await html('#container')).toBe(
  2156. '<!--teleport start--><!--teleport end-->',
  2157. )
  2158. },
  2159. E2E_TIMEOUT,
  2160. )
  2161. })
  2162. describe('transition with v-show', () => {
  2163. test(
  2164. 'named transition with v-show',
  2165. async () => {
  2166. await page().evaluate(() => {
  2167. const { createApp, ref } = (window as any).Vue
  2168. createApp({
  2169. template: `
  2170. <div id="container">
  2171. <transition name="test">
  2172. <div v-show="toggle" class="test">content</div>
  2173. </transition>
  2174. </div>
  2175. <button id="toggleBtn" @click="click">button</button>
  2176. `,
  2177. setup: () => {
  2178. const toggle = ref(true)
  2179. const click = () => (toggle.value = !toggle.value)
  2180. return { toggle, click }
  2181. },
  2182. }).mount('#app')
  2183. })
  2184. expect(await html('#container')).toBe('<div class="test">content</div>')
  2185. expect(await isVisible('.test')).toBe(true)
  2186. // leave
  2187. expect(await classWhenTransitionStart()).toStrictEqual([
  2188. 'test',
  2189. 'test-leave-from',
  2190. 'test-leave-active',
  2191. ])
  2192. await nextFrame()
  2193. expect(await classList('.test')).toStrictEqual([
  2194. 'test',
  2195. 'test-leave-active',
  2196. 'test-leave-to',
  2197. ])
  2198. await transitionFinish()
  2199. expect(await isVisible('.test')).toBe(false)
  2200. // enter
  2201. expect(await classWhenTransitionStart()).toStrictEqual([
  2202. 'test',
  2203. 'test-enter-from',
  2204. 'test-enter-active',
  2205. ])
  2206. await nextFrame()
  2207. expect(await classList('.test')).toStrictEqual([
  2208. 'test',
  2209. 'test-enter-active',
  2210. 'test-enter-to',
  2211. ])
  2212. await transitionFinish()
  2213. expect(await html('#container')).toBe(
  2214. '<div class="test" style="">content</div>',
  2215. )
  2216. },
  2217. E2E_TIMEOUT,
  2218. )
  2219. test(
  2220. 'transition events with v-show',
  2221. async () => {
  2222. const beforeLeaveSpy = vi.fn()
  2223. const onLeaveSpy = vi.fn()
  2224. const afterLeaveSpy = vi.fn()
  2225. const beforeEnterSpy = vi.fn()
  2226. const onEnterSpy = vi.fn()
  2227. const afterEnterSpy = vi.fn()
  2228. await page().exposeFunction('onLeaveSpy', onLeaveSpy)
  2229. await page().exposeFunction('onEnterSpy', onEnterSpy)
  2230. await page().exposeFunction('beforeLeaveSpy', beforeLeaveSpy)
  2231. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  2232. await page().exposeFunction('afterLeaveSpy', afterLeaveSpy)
  2233. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  2234. await page().evaluate(() => {
  2235. const {
  2236. beforeEnterSpy,
  2237. onEnterSpy,
  2238. afterEnterSpy,
  2239. beforeLeaveSpy,
  2240. onLeaveSpy,
  2241. afterLeaveSpy,
  2242. } = window as any
  2243. const { createApp, ref } = (window as any).Vue
  2244. createApp({
  2245. template: `
  2246. <div id="container">
  2247. <transition
  2248. name="test"
  2249. @before-enter="beforeEnterSpy()"
  2250. @enter="onEnterSpy()"
  2251. @after-enter="afterEnterSpy()"
  2252. @before-leave="beforeLeaveSpy()"
  2253. @leave="onLeaveSpy()"
  2254. @after-leave="afterLeaveSpy()">
  2255. <div v-show="toggle" class="test">content</div>
  2256. </transition>
  2257. </div>
  2258. <button id="toggleBtn" @click="click">button</button>
  2259. `,
  2260. setup: () => {
  2261. const toggle = ref(true)
  2262. const click = () => (toggle.value = !toggle.value)
  2263. return {
  2264. toggle,
  2265. click,
  2266. beforeEnterSpy,
  2267. onEnterSpy,
  2268. afterEnterSpy,
  2269. beforeLeaveSpy,
  2270. onLeaveSpy,
  2271. afterLeaveSpy,
  2272. }
  2273. },
  2274. }).mount('#app')
  2275. })
  2276. expect(await html('#container')).toBe('<div class="test">content</div>')
  2277. // leave
  2278. expect(await classWhenTransitionStart()).toStrictEqual([
  2279. 'test',
  2280. 'test-leave-from',
  2281. 'test-leave-active',
  2282. ])
  2283. expect(beforeLeaveSpy).toBeCalled()
  2284. expect(onLeaveSpy).toBeCalled()
  2285. expect(afterLeaveSpy).not.toBeCalled()
  2286. await nextFrame()
  2287. expect(await classList('.test')).toStrictEqual([
  2288. 'test',
  2289. 'test-leave-active',
  2290. 'test-leave-to',
  2291. ])
  2292. expect(afterLeaveSpy).not.toBeCalled()
  2293. await transitionFinish()
  2294. expect(await isVisible('.test')).toBe(false)
  2295. expect(afterLeaveSpy).toBeCalled()
  2296. // enter
  2297. expect(await classWhenTransitionStart()).toStrictEqual([
  2298. 'test',
  2299. 'test-enter-from',
  2300. 'test-enter-active',
  2301. ])
  2302. expect(beforeEnterSpy).toBeCalled()
  2303. expect(onEnterSpy).toBeCalled()
  2304. expect(afterEnterSpy).not.toBeCalled()
  2305. await nextFrame()
  2306. expect(await classList('.test')).toStrictEqual([
  2307. 'test',
  2308. 'test-enter-active',
  2309. 'test-enter-to',
  2310. ])
  2311. expect(afterEnterSpy).not.toBeCalled()
  2312. await transitionFinish()
  2313. expect(await html('#container')).toBe(
  2314. '<div class="test" style="">content</div>',
  2315. )
  2316. expect(afterEnterSpy).toBeCalled()
  2317. },
  2318. E2E_TIMEOUT,
  2319. )
  2320. test(
  2321. 'onLeaveCancelled (v-show only)',
  2322. async () => {
  2323. const onLeaveCancelledSpy = vi.fn()
  2324. await page().exposeFunction('onLeaveCancelledSpy', onLeaveCancelledSpy)
  2325. await page().evaluate(() => {
  2326. const { onLeaveCancelledSpy } = window as any
  2327. const { createApp, ref } = (window as any).Vue
  2328. createApp({
  2329. template: `
  2330. <div id="container">
  2331. <transition name="test" @leave-cancelled="onLeaveCancelledSpy()">
  2332. <div v-show="toggle" class="test">content</div>
  2333. </transition>
  2334. </div>
  2335. <button id="toggleBtn" @click="click">button</button>
  2336. `,
  2337. setup: () => {
  2338. const toggle = ref(true)
  2339. const click = () => (toggle.value = !toggle.value)
  2340. return { toggle, click, onLeaveCancelledSpy }
  2341. },
  2342. }).mount('#app')
  2343. })
  2344. expect(await html('#container')).toBe('<div class="test">content</div>')
  2345. expect(await isVisible('.test')).toBe(true)
  2346. // leave
  2347. expect(await classWhenTransitionStart()).toStrictEqual([
  2348. 'test',
  2349. 'test-leave-from',
  2350. 'test-leave-active',
  2351. ])
  2352. await nextFrame()
  2353. expect(await classList('.test')).toStrictEqual([
  2354. 'test',
  2355. 'test-leave-active',
  2356. 'test-leave-to',
  2357. ])
  2358. // cancel (enter)
  2359. expect(await classWhenTransitionStart()).toStrictEqual([
  2360. 'test',
  2361. 'test-enter-from',
  2362. 'test-enter-active',
  2363. ])
  2364. expect(onLeaveCancelledSpy).toBeCalled()
  2365. await nextFrame()
  2366. expect(await classList('.test')).toStrictEqual([
  2367. 'test',
  2368. 'test-enter-active',
  2369. 'test-enter-to',
  2370. ])
  2371. await transitionFinish()
  2372. expect(await html('#container')).toBe(
  2373. '<div class="test" style="">content</div>',
  2374. )
  2375. },
  2376. E2E_TIMEOUT,
  2377. )
  2378. test(
  2379. 'transition on appear with v-show',
  2380. async () => {
  2381. const beforeEnterSpy = vi.fn()
  2382. const onEnterSpy = vi.fn()
  2383. const afterEnterSpy = vi.fn()
  2384. await page().exposeFunction('onEnterSpy', onEnterSpy)
  2385. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  2386. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  2387. const appearClass = await page().evaluate(async () => {
  2388. const { createApp, ref } = (window as any).Vue
  2389. const { beforeEnterSpy, onEnterSpy, afterEnterSpy } = window as any
  2390. createApp({
  2391. template: `
  2392. <div id="container">
  2393. <transition name="test"
  2394. appear
  2395. appear-from-class="test-appear-from"
  2396. appear-to-class="test-appear-to"
  2397. appear-active-class="test-appear-active"
  2398. @before-enter="beforeEnterSpy()"
  2399. @enter="onEnterSpy()"
  2400. @after-enter="afterEnterSpy()">
  2401. <div v-show="toggle" class="test">content</div>
  2402. </transition>
  2403. </div>
  2404. <button id="toggleBtn" @click="click">button</button>
  2405. `,
  2406. setup: () => {
  2407. const toggle = ref(true)
  2408. const click = () => (toggle.value = !toggle.value)
  2409. return {
  2410. toggle,
  2411. click,
  2412. beforeEnterSpy,
  2413. onEnterSpy,
  2414. afterEnterSpy,
  2415. }
  2416. },
  2417. }).mount('#app')
  2418. return Promise.resolve().then(() => {
  2419. return document.querySelector('.test')!.className.split(/\s+/g)
  2420. })
  2421. })
  2422. expect(beforeEnterSpy).toBeCalledTimes(1)
  2423. expect(onEnterSpy).toBeCalledTimes(1)
  2424. expect(afterEnterSpy).toBeCalledTimes(0)
  2425. // appear
  2426. expect(appearClass).toStrictEqual([
  2427. 'test',
  2428. 'test-appear-from',
  2429. 'test-appear-active',
  2430. ])
  2431. await nextFrame()
  2432. expect(await classList('.test')).toStrictEqual([
  2433. 'test',
  2434. 'test-appear-active',
  2435. 'test-appear-to',
  2436. ])
  2437. await transitionFinish()
  2438. expect(await html('#container')).toBe('<div class="test">content</div>')
  2439. expect(beforeEnterSpy).toBeCalledTimes(1)
  2440. expect(onEnterSpy).toBeCalledTimes(1)
  2441. expect(afterEnterSpy).toBeCalledTimes(1)
  2442. // leave
  2443. expect(await classWhenTransitionStart()).toStrictEqual([
  2444. 'test',
  2445. 'test-leave-from',
  2446. 'test-leave-active',
  2447. ])
  2448. await nextFrame()
  2449. expect(await classList('.test')).toStrictEqual([
  2450. 'test',
  2451. 'test-leave-active',
  2452. 'test-leave-to',
  2453. ])
  2454. await transitionFinish()
  2455. expect(await isVisible('.test')).toBe(false)
  2456. // enter
  2457. expect(await classWhenTransitionStart()).toStrictEqual([
  2458. 'test',
  2459. 'test-enter-from',
  2460. 'test-enter-active',
  2461. ])
  2462. await nextFrame()
  2463. expect(await classList('.test')).toStrictEqual([
  2464. 'test',
  2465. 'test-enter-active',
  2466. 'test-enter-to',
  2467. ])
  2468. await transitionFinish()
  2469. expect(await html('#container')).toBe(
  2470. '<div class="test" style="">content</div>',
  2471. )
  2472. },
  2473. E2E_TIMEOUT,
  2474. )
  2475. // #4845
  2476. test(
  2477. 'transition events should not call onEnter with v-show false',
  2478. async () => {
  2479. const beforeEnterSpy = vi.fn()
  2480. const onEnterSpy = vi.fn()
  2481. const afterEnterSpy = vi.fn()
  2482. await page().exposeFunction('onEnterSpy', onEnterSpy)
  2483. await page().exposeFunction('beforeEnterSpy', beforeEnterSpy)
  2484. await page().exposeFunction('afterEnterSpy', afterEnterSpy)
  2485. await page().evaluate(() => {
  2486. const { beforeEnterSpy, onEnterSpy, afterEnterSpy } = window as any
  2487. const { createApp, ref } = (window as any).Vue
  2488. createApp({
  2489. template: `
  2490. <div id="container">
  2491. <transition
  2492. name="test"
  2493. appear
  2494. @before-enter="beforeEnterSpy()"
  2495. @enter="onEnterSpy()"
  2496. @after-enter="afterEnterSpy()">
  2497. <div v-show="toggle" class="test">content</div>
  2498. </transition>
  2499. </div>
  2500. <button id="toggleBtn" @click="click">button</button>
  2501. `,
  2502. setup: () => {
  2503. const toggle = ref(false)
  2504. const click = () => (toggle.value = !toggle.value)
  2505. return {
  2506. toggle,
  2507. click,
  2508. beforeEnterSpy,
  2509. onEnterSpy,
  2510. afterEnterSpy,
  2511. }
  2512. },
  2513. }).mount('#app')
  2514. })
  2515. await nextTick()
  2516. expect(await isVisible('.test')).toBe(false)
  2517. expect(beforeEnterSpy).toBeCalledTimes(0)
  2518. expect(onEnterSpy).toBeCalledTimes(0)
  2519. // enter
  2520. expect(await classWhenTransitionStart()).toStrictEqual([
  2521. 'test',
  2522. 'test-enter-from',
  2523. 'test-enter-active',
  2524. ])
  2525. expect(beforeEnterSpy).toBeCalledTimes(1)
  2526. expect(onEnterSpy).toBeCalledTimes(1)
  2527. expect(afterEnterSpy).not.toBeCalled()
  2528. await nextFrame()
  2529. expect(await classList('.test')).toStrictEqual([
  2530. 'test',
  2531. 'test-enter-active',
  2532. 'test-enter-to',
  2533. ])
  2534. expect(afterEnterSpy).not.toBeCalled()
  2535. await transitionFinish()
  2536. expect(await html('#container')).toBe(
  2537. '<div class="test" style="">content</div>',
  2538. )
  2539. expect(afterEnterSpy).toBeCalled()
  2540. },
  2541. E2E_TIMEOUT,
  2542. )
  2543. })
  2544. describe('explicit durations', () => {
  2545. test(
  2546. 'single value',
  2547. async () => {
  2548. await page().evaluate(duration => {
  2549. const { createApp, ref } = (window as any).Vue
  2550. createApp({
  2551. template: `
  2552. <div id="container">
  2553. <transition name="test" duration="${duration * 2}">
  2554. <div v-if="toggle" class="test">content</div>
  2555. </transition>
  2556. </div>
  2557. <button id="toggleBtn" @click="click">button</button>
  2558. `,
  2559. setup: () => {
  2560. const toggle = ref(true)
  2561. const click = () => (toggle.value = !toggle.value)
  2562. return { toggle, click }
  2563. },
  2564. }).mount('#app')
  2565. }, duration)
  2566. expect(await html('#container')).toBe('<div class="test">content</div>')
  2567. // leave
  2568. expect(await classWhenTransitionStart()).toStrictEqual([
  2569. 'test',
  2570. 'test-leave-from',
  2571. 'test-leave-active',
  2572. ])
  2573. await nextFrame()
  2574. expect(await classList('.test')).toStrictEqual([
  2575. 'test',
  2576. 'test-leave-active',
  2577. 'test-leave-to',
  2578. ])
  2579. await transitionFinish(duration * 2)
  2580. expect(await html('#container')).toBe('<!--v-if-->')
  2581. // enter
  2582. expect(await classWhenTransitionStart()).toStrictEqual([
  2583. 'test',
  2584. 'test-enter-from',
  2585. 'test-enter-active',
  2586. ])
  2587. await nextFrame()
  2588. expect(await classList('.test')).toStrictEqual([
  2589. 'test',
  2590. 'test-enter-active',
  2591. 'test-enter-to',
  2592. ])
  2593. await transitionFinish(duration * 2)
  2594. expect(await html('#container')).toBe('<div class="test">content</div>')
  2595. },
  2596. E2E_TIMEOUT,
  2597. )
  2598. test(
  2599. 'enter with explicit durations',
  2600. async () => {
  2601. await page().evaluate(duration => {
  2602. const { createApp, ref } = (window as any).Vue
  2603. createApp({
  2604. template: `
  2605. <div id="container">
  2606. <transition name="test" :duration="{ enter: ${duration * 2} }">
  2607. <div v-if="toggle" class="test">content</div>
  2608. </transition>
  2609. </div>
  2610. <button id="toggleBtn" @click="click">button</button>
  2611. `,
  2612. setup: () => {
  2613. const toggle = ref(true)
  2614. const click = () => (toggle.value = !toggle.value)
  2615. return { toggle, click }
  2616. },
  2617. }).mount('#app')
  2618. }, duration)
  2619. expect(await html('#container')).toBe('<div class="test">content</div>')
  2620. // leave
  2621. expect(await classWhenTransitionStart()).toStrictEqual([
  2622. 'test',
  2623. 'test-leave-from',
  2624. 'test-leave-active',
  2625. ])
  2626. await nextFrame()
  2627. expect(await classList('.test')).toStrictEqual([
  2628. 'test',
  2629. 'test-leave-active',
  2630. 'test-leave-to',
  2631. ])
  2632. await transitionFinish()
  2633. expect(await html('#container')).toBe('<!--v-if-->')
  2634. // enter
  2635. expect(await classWhenTransitionStart()).toStrictEqual([
  2636. 'test',
  2637. 'test-enter-from',
  2638. 'test-enter-active',
  2639. ])
  2640. await nextFrame()
  2641. expect(await classList('.test')).toStrictEqual([
  2642. 'test',
  2643. 'test-enter-active',
  2644. 'test-enter-to',
  2645. ])
  2646. await transitionFinish(duration * 2)
  2647. expect(await html('#container')).toBe('<div class="test">content</div>')
  2648. },
  2649. E2E_TIMEOUT,
  2650. )
  2651. test(
  2652. 'leave with explicit durations',
  2653. async () => {
  2654. await page().evaluate(duration => {
  2655. const { createApp, ref } = (window as any).Vue
  2656. createApp({
  2657. template: `
  2658. <div id="container">
  2659. <transition name="test" :duration="{ leave: ${duration * 2} }">
  2660. <div v-if="toggle" class="test">content</div>
  2661. </transition>
  2662. </div>
  2663. <button id="toggleBtn" @click="click">button</button>
  2664. `,
  2665. setup: () => {
  2666. const toggle = ref(true)
  2667. const click = () => (toggle.value = !toggle.value)
  2668. return { toggle, click }
  2669. },
  2670. }).mount('#app')
  2671. }, duration)
  2672. expect(await html('#container')).toBe('<div class="test">content</div>')
  2673. // leave
  2674. expect(await classWhenTransitionStart()).toStrictEqual([
  2675. 'test',
  2676. 'test-leave-from',
  2677. 'test-leave-active',
  2678. ])
  2679. await nextFrame()
  2680. expect(await classList('.test')).toStrictEqual([
  2681. 'test',
  2682. 'test-leave-active',
  2683. 'test-leave-to',
  2684. ])
  2685. await transitionFinish(duration * 2)
  2686. expect(await html('#container')).toBe('<!--v-if-->')
  2687. // enter
  2688. expect(await classWhenTransitionStart()).toStrictEqual([
  2689. 'test',
  2690. 'test-enter-from',
  2691. 'test-enter-active',
  2692. ])
  2693. await nextFrame()
  2694. expect(await classList('.test')).toStrictEqual([
  2695. 'test',
  2696. 'test-enter-active',
  2697. 'test-enter-to',
  2698. ])
  2699. await transitionFinish()
  2700. expect(await html('#container')).toBe('<div class="test">content</div>')
  2701. },
  2702. E2E_TIMEOUT,
  2703. )
  2704. test(
  2705. 'separate enter and leave',
  2706. async () => {
  2707. await page().evaluate(duration => {
  2708. const { createApp, ref } = (window as any).Vue
  2709. createApp({
  2710. template: `
  2711. <div id="container">
  2712. <transition name="test" :duration="{
  2713. enter: ${duration * 4},
  2714. leave: ${duration * 2}
  2715. }">
  2716. <div v-if="toggle" class="test">content</div>
  2717. </transition>
  2718. </div>
  2719. <button id="toggleBtn" @click="click">button</button>
  2720. `,
  2721. setup: () => {
  2722. const toggle = ref(true)
  2723. const click = () => (toggle.value = !toggle.value)
  2724. return { toggle, click }
  2725. },
  2726. }).mount('#app')
  2727. }, duration)
  2728. expect(await html('#container')).toBe('<div class="test">content</div>')
  2729. // leave
  2730. expect(await classWhenTransitionStart()).toStrictEqual([
  2731. 'test',
  2732. 'test-leave-from',
  2733. 'test-leave-active',
  2734. ])
  2735. await nextFrame()
  2736. expect(await classList('.test')).toStrictEqual([
  2737. 'test',
  2738. 'test-leave-active',
  2739. 'test-leave-to',
  2740. ])
  2741. await transitionFinish(duration * 2)
  2742. expect(await html('#container')).toBe('<!--v-if-->')
  2743. // enter
  2744. expect(await classWhenTransitionStart()).toStrictEqual([
  2745. 'test',
  2746. 'test-enter-from',
  2747. 'test-enter-active',
  2748. ])
  2749. await nextFrame()
  2750. expect(await classList('.test')).toStrictEqual([
  2751. 'test',
  2752. 'test-enter-active',
  2753. 'test-enter-to',
  2754. ])
  2755. await transitionFinish(duration * 4)
  2756. expect(await html('#container')).toBe('<div class="test">content</div>')
  2757. },
  2758. E2E_TIMEOUT,
  2759. )
  2760. test(
  2761. 'warn invalid durations',
  2762. async () => {
  2763. createApp({
  2764. template: `
  2765. <div id="container">
  2766. <transition name="test" :duration="NaN">
  2767. <div class="test">content</div>
  2768. </transition>
  2769. </div>
  2770. `,
  2771. }).mount(document.createElement('div'))
  2772. expect(
  2773. `[Vue warn]: <transition> explicit duration is NaN - ` +
  2774. 'the duration expression might be incorrect.',
  2775. ).toHaveBeenWarned()
  2776. createApp({
  2777. template: `
  2778. <div id="container">
  2779. <transition name="test" :duration="{
  2780. enter: {},
  2781. leave: {}
  2782. }">
  2783. <div class="test">content</div>
  2784. </transition>
  2785. </div>
  2786. `,
  2787. }).mount(document.createElement('div'))
  2788. expect(
  2789. `[Vue warn]: <transition> explicit duration is not a valid number - ` +
  2790. `got ${JSON.stringify({})}`,
  2791. ).toHaveBeenWarned()
  2792. },
  2793. E2E_TIMEOUT,
  2794. )
  2795. })
  2796. test('warn when used on multiple elements', async () => {
  2797. createApp({
  2798. render() {
  2799. return h(Transition, null, {
  2800. default: () => [h('div'), h('div')],
  2801. })
  2802. },
  2803. }).mount(document.createElement('div'))
  2804. expect(
  2805. '<transition> can only be used on a single element or component',
  2806. ).toHaveBeenWarned()
  2807. })
  2808. test('warn when invalid transition mode', () => {
  2809. createApp({
  2810. template: `
  2811. <div id="container">
  2812. <transition name="test" mode="none">
  2813. <div class="test">content</div>
  2814. </transition>
  2815. </div>
  2816. `,
  2817. }).mount(document.createElement('div'))
  2818. expect(`invalid <transition> mode: none`).toHaveBeenWarned()
  2819. })
  2820. // #3227
  2821. test(`HOC w/ merged hooks`, async () => {
  2822. const innerSpy = vi.fn()
  2823. const outerSpy = vi.fn()
  2824. const MyTransition = {
  2825. render(this: any) {
  2826. return h(
  2827. Transition,
  2828. {
  2829. onLeave(el, end) {
  2830. innerSpy()
  2831. end()
  2832. },
  2833. },
  2834. this.$slots.default,
  2835. )
  2836. },
  2837. }
  2838. const toggle = ref(true)
  2839. const root = document.createElement('div')
  2840. createApp({
  2841. render() {
  2842. return h(MyTransition, { onLeave: () => outerSpy() }, () =>
  2843. toggle.value ? h('div') : null,
  2844. )
  2845. },
  2846. }).mount(root)
  2847. expect(root.innerHTML).toBe(`<div></div>`)
  2848. toggle.value = false
  2849. await nextTick()
  2850. expect(innerSpy).toHaveBeenCalledTimes(1)
  2851. expect(outerSpy).toHaveBeenCalledTimes(1)
  2852. expect(root.innerHTML).toBe(`<!---->`)
  2853. })
  2854. test(
  2855. 'should work with dev root fragment',
  2856. async () => {
  2857. await page().evaluate(() => {
  2858. const { createApp, ref } = (window as any).Vue
  2859. createApp({
  2860. components: {
  2861. Comp: {
  2862. template: `
  2863. <!-- Broken! -->
  2864. <div><slot /></div>
  2865. `,
  2866. },
  2867. },
  2868. template: `
  2869. <div id="container">
  2870. <transition>
  2871. <Comp class="test" v-if="toggle">
  2872. <div>content</div>
  2873. </Comp>
  2874. </transition>
  2875. </div>
  2876. <button id="toggleBtn" @click="click">button</button>
  2877. `,
  2878. setup: () => {
  2879. const toggle = ref(true)
  2880. const click = () => (toggle.value = !toggle.value)
  2881. return { toggle, click }
  2882. },
  2883. }).mount('#app')
  2884. })
  2885. expect(await html('#container')).toBe(
  2886. '<!-- Broken! --><div class="test"><div>content</div></div>',
  2887. )
  2888. // leave
  2889. expect(await classWhenTransitionStart()).toStrictEqual([
  2890. 'test',
  2891. 'v-leave-from',
  2892. 'v-leave-active',
  2893. ])
  2894. await nextFrame()
  2895. expect(await classList('.test')).toStrictEqual([
  2896. 'test',
  2897. 'v-leave-active',
  2898. 'v-leave-to',
  2899. ])
  2900. await transitionFinish()
  2901. expect(await html('#container')).toBe('<!--v-if-->')
  2902. // enter
  2903. expect(await classWhenTransitionStart()).toStrictEqual([
  2904. 'test',
  2905. 'v-enter-from',
  2906. 'v-enter-active',
  2907. ])
  2908. await nextFrame()
  2909. expect(await classList('.test')).toStrictEqual([
  2910. 'test',
  2911. 'v-enter-active',
  2912. 'v-enter-to',
  2913. ])
  2914. await transitionFinish()
  2915. expect(await html('#container')).toBe(
  2916. '<!-- Broken! --><div class="test"><div>content</div></div>',
  2917. )
  2918. },
  2919. E2E_TIMEOUT,
  2920. )
  2921. })