Transition.spec.ts 104 KB

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