hmr.spec.ts 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798
  1. import {
  2. type HMRRuntime,
  3. computed,
  4. createApp,
  5. currentInstance,
  6. h,
  7. inject,
  8. nextTick,
  9. onActivated,
  10. onDeactivated,
  11. onMounted,
  12. onUnmounted,
  13. popWarningContext,
  14. provide,
  15. ref,
  16. setCurrentInstance,
  17. toDisplayString,
  18. warn,
  19. watchEffect,
  20. } from '@vue/runtime-dom'
  21. import { compileToVaporRender as compileToFunction, makeRender } from './_utils'
  22. import {
  23. createComponent,
  24. createSlot,
  25. createTemplateRefSetter,
  26. createVaporApp,
  27. defineVaporAsyncComponent,
  28. defineVaporComponent,
  29. delegateEvents,
  30. renderEffect,
  31. setText,
  32. template,
  33. vaporInteropPlugin,
  34. } from '@vue/runtime-vapor'
  35. import { BindingTypes } from '@vue/compiler-core'
  36. import type { VaporComponent } from '../src/component'
  37. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  38. const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
  39. const define = makeRender()
  40. const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
  41. const triggerEvent = (type: string, el: Element) => {
  42. const event = new Event(type, { bubbles: true })
  43. el.dispatchEvent(event)
  44. }
  45. delegateEvents('click')
  46. beforeEach(() => {
  47. document.body.innerHTML = ''
  48. })
  49. describe('hot module replacement', () => {
  50. test('inject global runtime', () => {
  51. expect(createRecord).toBeDefined()
  52. expect(rerender).toBeDefined()
  53. expect(reload).toBeDefined()
  54. })
  55. test('createRecord', () => {
  56. expect(createRecord('test1', {})).toBe(true)
  57. // if id has already been created, should return false
  58. expect(createRecord('test1', {})).toBe(false)
  59. })
  60. test('rerender', async () => {
  61. const root = document.createElement('div')
  62. const parentId = 'test2-parent'
  63. const childId = 'test2-child'
  64. document.body.appendChild(root)
  65. const Child = defineVaporComponent({
  66. __hmrId: childId,
  67. render: compileToFunction('<div><slot/></div>'),
  68. })
  69. createRecord(childId, Child as any)
  70. const Parent = defineVaporComponent({
  71. __hmrId: parentId,
  72. components: { Child },
  73. setup() {
  74. const count = ref(0)
  75. return { count }
  76. },
  77. render: compileToFunction(
  78. `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`,
  79. ),
  80. })
  81. createRecord(parentId, Parent as any)
  82. const { mount } = define(Parent).create()
  83. mount(root)
  84. expect(root.innerHTML).toBe(`<div>0<div>0<!--slot--></div></div>`)
  85. // Perform some state change. This change should be preserved after the
  86. // re-render!
  87. // triggerEvent(root.children[0] as TestElement, 'click')
  88. triggerEvent('click', root.children[0])
  89. await nextTick()
  90. expect(root.innerHTML).toBe(`<div>1<div>1<!--slot--></div></div>`)
  91. // Update text while preserving state
  92. rerender(
  93. parentId,
  94. compileToFunction(
  95. `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`,
  96. ),
  97. )
  98. expect(root.innerHTML).toBe(`<div>1!<div>1<!--slot--></div></div>`)
  99. // Should force child update on slot content change
  100. rerender(
  101. parentId,
  102. compileToFunction(
  103. `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`,
  104. ),
  105. )
  106. expect(root.innerHTML).toBe(`<div>1!<div>1!<!--slot--></div></div>`)
  107. // Should force update element children despite block optimization
  108. rerender(
  109. parentId,
  110. compileToFunction(
  111. `<div @click="count++">{{ count }}<span>{{ count }}</span>
  112. <Child>{{ count }}!</Child>
  113. </div>`,
  114. ),
  115. )
  116. expect(root.innerHTML).toBe(
  117. `<div>1<span>1</span><div>1!<!--slot--></div></div>`,
  118. )
  119. // Should force update child slot elements
  120. rerender(
  121. parentId,
  122. compileToFunction(
  123. `<div @click="count++">
  124. <Child><span>{{ count }}</span></Child>
  125. </div>`,
  126. ),
  127. )
  128. expect(root.innerHTML).toBe(
  129. `<div><div><span>1</span><!--slot--></div></div>`,
  130. )
  131. })
  132. test('reload', async () => {
  133. const root = document.createElement('div')
  134. const childId = 'test3-child'
  135. const unmountSpy = vi.fn()
  136. const mountSpy = vi.fn()
  137. const Child = defineVaporComponent({
  138. __hmrId: childId,
  139. setup() {
  140. onUnmounted(unmountSpy)
  141. const count = ref(0)
  142. return { count }
  143. },
  144. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  145. })
  146. createRecord(childId, Child as any)
  147. const Parent = defineVaporComponent({
  148. __hmrId: 'parentId',
  149. render: () => createComponent(Child),
  150. })
  151. define(Parent).create().mount(root)
  152. expect(root.innerHTML).toBe(`<div>0</div>`)
  153. reload(childId, {
  154. __hmrId: childId,
  155. setup() {
  156. onMounted(mountSpy)
  157. const count = ref(1)
  158. return { count }
  159. },
  160. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  161. })
  162. await nextTick()
  163. expect(root.innerHTML).toBe(`<div>1</div>`)
  164. expect(unmountSpy).toHaveBeenCalledTimes(1)
  165. expect(mountSpy).toHaveBeenCalledTimes(1)
  166. })
  167. test('reload child should preserve parent setup effects', async () => {
  168. const root = document.createElement('div')
  169. const childId = 'test-reload-child-preserve-parent-effects'
  170. const parentCount = ref(0)
  171. const spy = vi.fn()
  172. const Child = defineVaporComponent({
  173. __hmrId: childId,
  174. render: () => template('<div>old</div>')(),
  175. })
  176. createRecord(childId, Child as any)
  177. const Parent = defineVaporComponent({
  178. setup() {
  179. watchEffect(() => spy(parentCount.value))
  180. },
  181. render: () => createComponent(Child),
  182. })
  183. createVaporApp(Parent).mount(root)
  184. expect(root.innerHTML).toBe(`<div>old</div>`)
  185. expect(spy).toHaveBeenLastCalledWith(0)
  186. reload(childId, {
  187. __vapor: true,
  188. __hmrId: childId,
  189. render: () => template('<div>new</div>')(),
  190. })
  191. await nextTick()
  192. expect(root.innerHTML).toBe(`<div>new</div>`)
  193. parentCount.value++
  194. await nextTick()
  195. expect(spy).toHaveBeenLastCalledWith(1)
  196. })
  197. test('reload root vapor component should preserve appContext provide/inject', async () => {
  198. const root = document.createElement('div')
  199. const appId = 'test-root-reload-app-context'
  200. const Child = defineVaporComponent({
  201. setup() {
  202. const msg = inject('msg')
  203. return { msg }
  204. },
  205. render: compileToFunction(`<div>{{ msg }}</div>`),
  206. })
  207. const App = defineVaporComponent({
  208. __hmrId: appId,
  209. render: () => createComponent(Child),
  210. })
  211. createRecord(appId, App as any)
  212. const app = createVaporApp(App)
  213. app.provide('msg', 'app-injected')
  214. app.mount(root)
  215. expect(root.innerHTML).toBe(`<div>app-injected</div>`)
  216. reload(appId, {
  217. __vapor: true,
  218. __hmrId: appId,
  219. render: () => createComponent(Child),
  220. })
  221. await nextTick()
  222. expect(root.innerHTML).toBe(`<div>app-injected</div>`)
  223. })
  224. test('reload root vapor component should update app instance for unmount', async () => {
  225. const root = document.createElement('div')
  226. const appId = 'test-root-reload-app-unmount'
  227. const oldUnmountSpy = vi.fn()
  228. const newUnmountSpy = vi.fn()
  229. const App = defineVaporComponent({
  230. __hmrId: appId,
  231. setup() {
  232. onUnmounted(oldUnmountSpy)
  233. },
  234. render: () => template(`<div>old</div>`)(),
  235. })
  236. createRecord(appId, App as any)
  237. const app = createVaporApp(App)
  238. app.mount(root)
  239. expect(root.innerHTML).toBe(`<div>old</div>`)
  240. reload(appId, {
  241. __vapor: true,
  242. __hmrId: appId,
  243. setup() {
  244. onUnmounted(newUnmountSpy)
  245. },
  246. render: () => template(`<div>new</div>`)(),
  247. })
  248. await nextTick()
  249. expect(root.innerHTML).toBe(`<div>new</div>`)
  250. expect(oldUnmountSpy).toHaveBeenCalledTimes(1)
  251. app.unmount()
  252. await nextTick()
  253. expect(root.innerHTML).toBe(``)
  254. expect(newUnmountSpy).toHaveBeenCalledTimes(1)
  255. })
  256. test('failed rerender restores current instance and warning context', () => {
  257. const root = document.createElement('div')
  258. const id = 'test-rerender-restore-context'
  259. const warnHandler = vi.fn()
  260. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
  261. const Comp = defineVaporComponent({
  262. __hmrId: id,
  263. render: () => template('ok')(),
  264. })
  265. createRecord(id, Comp as any)
  266. const app = createVaporApp(Comp)
  267. app.config.warnHandler = warnHandler
  268. app.mount(root)
  269. expect(currentInstance).toBe(null)
  270. rerender(id, () => {
  271. throw new Error('hmr rerender error')
  272. })
  273. warnHandler.mockClear()
  274. const leakedInstance = currentInstance
  275. setCurrentInstance(null, undefined)
  276. warn('after failed hmr')
  277. popWarningContext()
  278. errorSpy.mockRestore()
  279. expect(
  280. '[HMR] Something went wrong during Vue component hot-reload.',
  281. ).toHaveBeenWarned()
  282. expect('[Vue warn]: after failed hmr').toHaveBeenWarned()
  283. expect(leakedInstance).toBe(null)
  284. expect(warnHandler).not.toHaveBeenCalled()
  285. })
  286. test('failed reload restores current instance', () => {
  287. const root = document.createElement('div')
  288. const childId = 'test-reload-restore-context-child'
  289. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
  290. const Child = defineVaporComponent({
  291. __hmrId: childId,
  292. render: () => template('old')(),
  293. })
  294. createRecord(childId, Child as any)
  295. const Parent = defineVaporComponent({
  296. render: () => createComponent(Child),
  297. })
  298. createVaporApp(Parent).mount(root)
  299. expect(currentInstance).toBe(null)
  300. reload(childId, {
  301. __vapor: true,
  302. __hmrId: childId,
  303. setup() {
  304. throw new Error('hmr reload error')
  305. },
  306. render: () => template('new')(),
  307. })
  308. const leakedInstance = currentInstance
  309. setCurrentInstance(null, undefined)
  310. errorSpy.mockRestore()
  311. expect(
  312. '[Vue warn]: Unhandled error during execution of setup function',
  313. ).toHaveBeenWarned()
  314. expect(
  315. '[Vue warn]: Unhandled error during execution of render function',
  316. ).toHaveBeenWarned()
  317. expect(
  318. '[HMR] Something went wrong during Vue component hot-reload.',
  319. ).toHaveBeenWarned()
  320. expect(leakedInstance).toBe(null)
  321. })
  322. test('reload KeepAlive slot', async () => {
  323. const root = document.createElement('div')
  324. document.body.appendChild(root)
  325. const childId = 'test-child-keep-alive'
  326. const unmountSpy = vi.fn()
  327. const mountSpy = vi.fn()
  328. const activeSpy = vi.fn()
  329. const deactivatedSpy = vi.fn()
  330. const Child = defineVaporComponent({
  331. __hmrId: childId,
  332. setup() {
  333. onUnmounted(unmountSpy)
  334. const count = ref(0)
  335. return { count }
  336. },
  337. render: compileToFunction(`<div>{{ count }}</div>`),
  338. })
  339. createRecord(childId, Child as any)
  340. const Parent = defineVaporComponent({
  341. __hmrId: 'parentId',
  342. components: { Child },
  343. setup() {
  344. const toggle = ref(true)
  345. return { toggle }
  346. },
  347. render: compileToFunction(
  348. `<button @click="toggle = !toggle" />
  349. <KeepAlive><Child v-if="toggle" /></KeepAlive>`,
  350. ),
  351. })
  352. define(Parent).create().mount(root)
  353. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  354. reload(childId, {
  355. __hmrId: childId,
  356. __vapor: true,
  357. setup() {
  358. onMounted(mountSpy)
  359. onUnmounted(unmountSpy)
  360. onActivated(activeSpy)
  361. onDeactivated(deactivatedSpy)
  362. const count = ref(1)
  363. return { count }
  364. },
  365. render: compileToFunction(`<div>{{ count }}</div>`),
  366. })
  367. await nextTick()
  368. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  369. expect(unmountSpy).toHaveBeenCalledTimes(1)
  370. expect(mountSpy).toHaveBeenCalledTimes(1)
  371. expect(activeSpy).toHaveBeenCalledTimes(1)
  372. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  373. // should not unmount when toggling
  374. triggerEvent('click', root.children[0] as Element)
  375. await nextTick()
  376. expect(unmountSpy).toHaveBeenCalledTimes(1)
  377. expect(mountSpy).toHaveBeenCalledTimes(1)
  378. expect(activeSpy).toHaveBeenCalledTimes(1)
  379. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  380. // should not mount when toggling
  381. triggerEvent('click', root.children[0] as Element)
  382. await nextTick()
  383. expect(unmountSpy).toHaveBeenCalledTimes(1)
  384. expect(mountSpy).toHaveBeenCalledTimes(1)
  385. expect(activeSpy).toHaveBeenCalledTimes(2)
  386. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  387. })
  388. test('reload deactivated KeepAlive child', async () => {
  389. const root = document.createElement('div')
  390. document.body.appendChild(root)
  391. const childId = 'test-child-keep-alive-deactivated'
  392. const oldUnmountSpy = vi.fn()
  393. const oldActiveSpy = vi.fn()
  394. const oldDeactivatedSpy = vi.fn()
  395. const newUnmountSpy = vi.fn()
  396. const newMountSpy = vi.fn()
  397. const newActiveSpy = vi.fn()
  398. const newDeactivatedSpy = vi.fn()
  399. const Child = defineVaporComponent({
  400. __hmrId: childId,
  401. setup() {
  402. onUnmounted(oldUnmountSpy)
  403. onActivated(oldActiveSpy)
  404. onDeactivated(oldDeactivatedSpy)
  405. const count = ref(0)
  406. return { count }
  407. },
  408. render: compileToFunction(`<div>{{ count }}</div>`),
  409. })
  410. createRecord(childId, Child as any)
  411. const Parent = defineVaporComponent({
  412. __hmrId: 'parentId-keep-alive-deactivated',
  413. components: { Child },
  414. setup() {
  415. const toggle = ref(true)
  416. return { toggle }
  417. },
  418. render: compileToFunction(
  419. `<button @click="toggle = !toggle" />
  420. <KeepAlive><Child v-if="toggle" /></KeepAlive>`,
  421. ),
  422. })
  423. define(Parent).create().mount(root)
  424. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  425. expect(oldActiveSpy).toHaveBeenCalledTimes(1)
  426. expect(oldDeactivatedSpy).toHaveBeenCalledTimes(0)
  427. expect(oldUnmountSpy).toHaveBeenCalledTimes(0)
  428. // deactivate and move child into KeepAlive cache
  429. triggerEvent('click', root.children[0] as Element)
  430. await nextTick()
  431. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  432. expect(oldDeactivatedSpy).toHaveBeenCalledTimes(1)
  433. expect(oldUnmountSpy).toHaveBeenCalledTimes(0)
  434. // reload while child is cached but inactive
  435. reload(childId, {
  436. __hmrId: childId,
  437. __vapor: true,
  438. setup() {
  439. onMounted(newMountSpy)
  440. onUnmounted(newUnmountSpy)
  441. onActivated(newActiveSpy)
  442. onDeactivated(newDeactivatedSpy)
  443. const count = ref(1)
  444. return { count }
  445. },
  446. render: compileToFunction(`<div>{{ count }}</div>`),
  447. })
  448. await nextTick()
  449. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  450. // old cached instance should be unmounted during KeepAlive HMR rerender
  451. expect(oldUnmountSpy).toHaveBeenCalledTimes(1)
  452. // re-activate should render the new component instance
  453. triggerEvent('click', root.children[0] as Element)
  454. await nextTick()
  455. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  456. expect(newMountSpy).toHaveBeenCalledTimes(1)
  457. expect(newActiveSpy).toHaveBeenCalledTimes(1)
  458. expect(newDeactivatedSpy).toHaveBeenCalledTimes(0)
  459. // subsequent toggles should use KeepAlive cache for the new instance
  460. triggerEvent('click', root.children[0] as Element)
  461. await nextTick()
  462. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  463. expect(newMountSpy).toHaveBeenCalledTimes(1)
  464. expect(newActiveSpy).toHaveBeenCalledTimes(1)
  465. expect(newDeactivatedSpy).toHaveBeenCalledTimes(1)
  466. triggerEvent('click', root.children[0] as Element)
  467. await nextTick()
  468. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  469. expect(newMountSpy).toHaveBeenCalledTimes(1)
  470. expect(newActiveSpy).toHaveBeenCalledTimes(2)
  471. expect(newDeactivatedSpy).toHaveBeenCalledTimes(1)
  472. expect(newUnmountSpy).toHaveBeenCalledTimes(0)
  473. })
  474. test('reload KeepAlive slot in Transition', async () => {
  475. const root = document.createElement('div')
  476. document.body.appendChild(root)
  477. const childId = 'test-transition-keep-alive-reload'
  478. const unmountSpy = vi.fn()
  479. const mountSpy = vi.fn()
  480. const activeSpy = vi.fn()
  481. const deactivatedSpy = vi.fn()
  482. const Child = defineVaporComponent({
  483. __hmrId: childId,
  484. setup() {
  485. onUnmounted(unmountSpy)
  486. const count = ref(0)
  487. return { count }
  488. },
  489. render: compileToFunction(`<div>{{ count }}</div>`),
  490. })
  491. createRecord(childId, Child as any)
  492. const Parent = defineVaporComponent({
  493. __hmrId: 'parentId',
  494. components: { Child },
  495. setup() {
  496. const toggle = ref(true)
  497. function onLeave(_: any, done: Function) {
  498. setTimeout(done, 0)
  499. }
  500. return { toggle, onLeave }
  501. },
  502. render: compileToFunction(
  503. `<button @click="toggle = !toggle" />
  504. <Transition @leave="onLeave">
  505. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  506. </Transition>`,
  507. ),
  508. })
  509. define(Parent).create().mount(root)
  510. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  511. reload(childId, {
  512. __hmrId: childId,
  513. __vapor: true,
  514. setup() {
  515. onMounted(mountSpy)
  516. onUnmounted(unmountSpy)
  517. onActivated(activeSpy)
  518. onDeactivated(deactivatedSpy)
  519. const count = ref(1)
  520. return { count }
  521. },
  522. render: compileToFunction(`<div>{{ count }}</div>`),
  523. })
  524. await nextTick()
  525. await new Promise(r => setTimeout(r, 0))
  526. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  527. expect(unmountSpy).toHaveBeenCalledTimes(1)
  528. expect(mountSpy).toHaveBeenCalledTimes(1)
  529. expect(activeSpy).toHaveBeenCalledTimes(1)
  530. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  531. // should not unmount when toggling
  532. triggerEvent('click', root.children[0] as Element)
  533. await nextTick()
  534. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  535. expect(unmountSpy).toHaveBeenCalledTimes(1)
  536. expect(mountSpy).toHaveBeenCalledTimes(1)
  537. expect(activeSpy).toHaveBeenCalledTimes(1)
  538. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  539. // should not mount when toggling
  540. triggerEvent('click', root.children[0] as Element)
  541. await nextTick()
  542. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  543. expect(unmountSpy).toHaveBeenCalledTimes(1)
  544. expect(mountSpy).toHaveBeenCalledTimes(1)
  545. expect(activeSpy).toHaveBeenCalledTimes(2)
  546. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  547. })
  548. test('reload KeepAlive slot in Transition with out-in', async () => {
  549. const root = document.createElement('div')
  550. document.body.appendChild(root)
  551. const childId = 'test-transition-keep-alive-reload-with-out-in'
  552. const unmountSpy = vi.fn()
  553. const mountSpy = vi.fn()
  554. const activeSpy = vi.fn()
  555. const deactivatedSpy = vi.fn()
  556. const Child = defineVaporComponent({
  557. name: 'original',
  558. __hmrId: childId,
  559. setup() {
  560. onUnmounted(unmountSpy)
  561. const count = ref(0)
  562. return { count }
  563. },
  564. render: compileToFunction(`<div>{{ count }}</div>`),
  565. })
  566. createRecord(childId, Child as any)
  567. const Parent = defineVaporComponent({
  568. components: { Child },
  569. setup() {
  570. function onLeave(_: any, done: Function) {
  571. setTimeout(done, 0)
  572. }
  573. const toggle = ref(true)
  574. return { toggle, onLeave }
  575. },
  576. render: compileToFunction(
  577. `<button @click="toggle = !toggle" />
  578. <Transition mode="out-in" @leave="onLeave">
  579. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  580. </Transition>`,
  581. ),
  582. })
  583. define(Parent).create().mount(root)
  584. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  585. reload(childId, {
  586. name: 'updated',
  587. __hmrId: childId,
  588. __vapor: true,
  589. setup() {
  590. onMounted(mountSpy)
  591. onUnmounted(unmountSpy)
  592. onActivated(activeSpy)
  593. onDeactivated(deactivatedSpy)
  594. const count = ref(1)
  595. return { count }
  596. },
  597. render: compileToFunction(`<div>{{ count }}</div>`),
  598. })
  599. await nextTick()
  600. await new Promise(r => setTimeout(r, 0))
  601. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  602. expect(unmountSpy).toHaveBeenCalledTimes(1)
  603. expect(mountSpy).toHaveBeenCalledTimes(1)
  604. expect(activeSpy).toHaveBeenCalledTimes(1)
  605. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  606. // should not unmount when toggling
  607. triggerEvent('click', root.children[0] as Element)
  608. await nextTick()
  609. await new Promise(r => setTimeout(r, 0))
  610. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  611. expect(unmountSpy).toHaveBeenCalledTimes(1)
  612. expect(mountSpy).toHaveBeenCalledTimes(1)
  613. expect(activeSpy).toHaveBeenCalledTimes(1)
  614. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  615. // should not mount when toggling
  616. triggerEvent('click', root.children[0] as Element)
  617. await nextTick()
  618. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  619. expect(unmountSpy).toHaveBeenCalledTimes(1)
  620. expect(mountSpy).toHaveBeenCalledTimes(1)
  621. expect(activeSpy).toHaveBeenCalledTimes(2)
  622. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  623. })
  624. test('reload child through parent rerender', async () => {
  625. const root = document.createElement('div')
  626. document.body.appendChild(root)
  627. const childId = 'test-child-6930'
  628. const unmountSpy = vi.fn()
  629. const mountSpy = vi.fn()
  630. const Child = defineVaporComponent({
  631. __hmrId: childId,
  632. setup(_, { expose }) {
  633. const count = ref(0)
  634. expose({
  635. count,
  636. })
  637. onUnmounted(unmountSpy)
  638. return { count }
  639. },
  640. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  641. })
  642. createRecord(childId, Child as any)
  643. const Parent = defineVaporComponent({
  644. setup() {
  645. const com1 = ref()
  646. const changeRef1 = (value: any) => (com1.value = value)
  647. const com2 = ref()
  648. const changeRef2 = (value: any) => (com2.value = value)
  649. const setRef = createTemplateRefSetter()
  650. const n0 = createComponent(Child)
  651. setRef(n0, changeRef1)
  652. const n1 = createComponent(Child)
  653. setRef(n1, changeRef2)
  654. const n2 = template(' ')() as any
  655. renderEffect(() => {
  656. setText(n2, toDisplayString(com1.value.count))
  657. })
  658. return [n0, n1, n2]
  659. },
  660. })
  661. define(Parent).create().mount(root)
  662. await nextTick()
  663. expect(root.innerHTML).toBe(`<div>0</div><div>0</div>0`)
  664. reload(childId, {
  665. __hmrId: childId,
  666. __vapor: true,
  667. setup(_, { expose }) {
  668. onMounted(mountSpy)
  669. const count = ref(1)
  670. expose({
  671. count,
  672. })
  673. return { count }
  674. },
  675. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  676. })
  677. await nextTick()
  678. await nextTick()
  679. expect(root.innerHTML).toBe(`<div>1</div><div>1</div>1`)
  680. expect(unmountSpy).toHaveBeenCalledTimes(2)
  681. expect(mountSpy).toHaveBeenCalledTimes(2)
  682. })
  683. test('reload multiple children under same vapor parent should rerender parent once', async () => {
  684. const root = document.createElement('div')
  685. const childId = 'test-child-reload-same-vapor-parent'
  686. const Child = defineVaporComponent({
  687. __hmrId: childId,
  688. render: () => template('<div>old</div>')(),
  689. })
  690. createRecord(childId, Child as any)
  691. let parentRenderCount = 0
  692. const Parent = defineVaporComponent({
  693. render() {
  694. parentRenderCount++
  695. return [createComponent(Child), createComponent(Child)]
  696. },
  697. })
  698. createVaporApp(Parent).mount(root)
  699. expect(root.innerHTML).toBe(`<div>old</div><div>old</div>`)
  700. expect(parentRenderCount).toBe(1)
  701. reload(childId, {
  702. __vapor: true,
  703. __hmrId: childId,
  704. render: () => template('<div>new</div>')(),
  705. })
  706. await nextTick()
  707. expect(root.innerHTML).toBe(`<div>new</div><div>new</div>`)
  708. expect(parentRenderCount).toBe(2)
  709. })
  710. test('reload vapor child under dirty ancestor should not rerender stale owner', async () => {
  711. const root = document.createElement('div')
  712. const id = 'test-child-reload-dirty-ancestor'
  713. let Child: any
  714. const Wrapper = defineVaporComponent({
  715. render() {
  716. return createComponent(Child, { nested: () => true })
  717. },
  718. })
  719. Child = defineVaporComponent({
  720. __hmrId: id,
  721. props: ['nested'],
  722. setup(props: any) {
  723. return { nested: props.nested }
  724. },
  725. render: compileToFunction(
  726. `<div>old {{ nested ? 'nested' : 'root' }}</div><Wrapper v-if="!nested" />`,
  727. ),
  728. })
  729. Child.components = { Wrapper }
  730. createRecord(id, Child)
  731. createVaporApp(Child, { nested: () => false }).mount(root)
  732. expect(root.textContent).toBe(`old rootold nested`)
  733. const NewChild: any = {
  734. __vapor: true,
  735. __hmrId: id,
  736. props: ['nested'],
  737. setup(props: any) {
  738. return { nested: props.nested }
  739. },
  740. render: compileToFunction(
  741. `<div>new {{ nested ? 'nested' : 'root' }}</div><Wrapper v-if="!nested" />`,
  742. ),
  743. }
  744. NewChild.components = { Wrapper }
  745. reload(id, NewChild)
  746. await nextTick()
  747. expect(root.textContent).toBe(`new rootnew nested`)
  748. })
  749. test('static el reference', async () => {
  750. const root = document.createElement('div')
  751. document.body.appendChild(root)
  752. const id = 'test-static-el'
  753. const template = `<div>
  754. <div>{{ count }}</div>
  755. <button @click="count++">++</button>
  756. </div>`
  757. const Comp = defineVaporComponent({
  758. __hmrId: id,
  759. setup() {
  760. const count = ref(0)
  761. return { count }
  762. },
  763. render: compileToFunction(template),
  764. })
  765. createRecord(id, Comp as any)
  766. define(Comp).create().mount(root)
  767. expect(root.innerHTML).toBe(`<div><div>0</div><button>++</button></div>`)
  768. // 1. click to trigger update
  769. triggerEvent('click', root.children[0].children[1] as Element)
  770. await nextTick()
  771. expect(root.innerHTML).toBe(`<div><div>1</div><button>++</button></div>`)
  772. // 2. trigger HMR
  773. rerender(
  774. id,
  775. compileToFunction(template.replace(`<button`, `<button class="foo"`)),
  776. )
  777. expect(root.innerHTML).toBe(
  778. `<div><div>1</div><button class="foo">++</button></div>`,
  779. )
  780. })
  781. test('force update child component w/ static props', () => {
  782. const root = document.createElement('div')
  783. const parentId = 'test-force-props-parent'
  784. const childId = 'test-force-props-child'
  785. const Child = defineVaporComponent({
  786. __hmrId: childId,
  787. props: {
  788. msg: String,
  789. },
  790. render: compileToFunction(`<div>{{ msg }}</div>`, {
  791. bindingMetadata: {
  792. msg: BindingTypes.PROPS,
  793. },
  794. }),
  795. })
  796. createRecord(childId, Child as any)
  797. const Parent = defineVaporComponent({
  798. __hmrId: parentId,
  799. components: { Child },
  800. render: compileToFunction(`<Child msg="foo" />`),
  801. })
  802. createRecord(parentId, Parent as any)
  803. define(Parent).create().mount(root)
  804. expect(root.innerHTML).toBe(`<div>foo</div>`)
  805. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  806. expect(root.innerHTML).toBe(`<div>bar</div>`)
  807. })
  808. test('remove static class from parent', () => {
  809. const root = document.createElement('div')
  810. const parentId = 'test-force-class-parent'
  811. const childId = 'test-force-class-child'
  812. const Child = defineVaporComponent({
  813. __hmrId: childId,
  814. render: compileToFunction(`<div>child</div>`),
  815. })
  816. createRecord(childId, Child as any)
  817. const Parent = defineVaporComponent({
  818. __hmrId: parentId,
  819. components: { Child },
  820. render: compileToFunction(`<Child class="test" />`),
  821. })
  822. createRecord(parentId, Parent as any)
  823. define(Parent).create().mount(root)
  824. expect(root.innerHTML).toBe(`<div class="test">child</div>`)
  825. rerender(parentId, compileToFunction(`<Child/>`))
  826. expect(root.innerHTML).toBe(`<div>child</div>`)
  827. })
  828. test('rerender if any parent in the parent chain', () => {
  829. const root = document.createElement('div')
  830. const parent = 'test-force-props-parent-'
  831. const childId = 'test-force-props-child'
  832. const numberOfParents = 5
  833. const Child = defineVaporComponent({
  834. __hmrId: childId,
  835. render: compileToFunction(`<div>child</div>`),
  836. })
  837. createRecord(childId, Child as any)
  838. const components: VaporComponent[] = []
  839. for (let i = 0; i < numberOfParents; i++) {
  840. const parentId = `${parent}${i}`
  841. const parentComp: VaporComponent = {
  842. __vapor: true,
  843. __hmrId: parentId,
  844. }
  845. components.push(parentComp)
  846. if (i === 0) {
  847. parentComp.render = compileToFunction(`<Child />`)
  848. parentComp.components = {
  849. Child,
  850. }
  851. } else {
  852. parentComp.render = compileToFunction(`<Parent />`)
  853. parentComp.components = {
  854. Parent: components[i - 1],
  855. }
  856. }
  857. createRecord(parentId, parentComp as any)
  858. }
  859. const last = components[components.length - 1]
  860. define(last).create().mount(root)
  861. expect(root.innerHTML).toBe(`<div>child</div>`)
  862. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  863. expect(root.innerHTML).toBe(`<div class="test">child</div>`)
  864. })
  865. test('rerender with Teleport', () => {
  866. const root = document.createElement('div')
  867. const target = document.createElement('div')
  868. document.body.appendChild(root)
  869. document.body.appendChild(target)
  870. const parentId = 'parent-teleport'
  871. const Child = defineVaporComponent({
  872. setup() {
  873. return { target }
  874. },
  875. render: compileToFunction(`
  876. <teleport :to="target">
  877. <div>
  878. <slot/>
  879. </div>
  880. </teleport>
  881. `),
  882. })
  883. const Parent = {
  884. __vapor: true,
  885. __hmrId: parentId,
  886. components: { Child },
  887. render: compileToFunction(`
  888. <Child>
  889. <template #default>
  890. <div>1</div>
  891. </template>
  892. </Child>
  893. `),
  894. }
  895. createRecord(parentId, Parent as any)
  896. define(Parent).create().mount(root)
  897. expect(root.innerHTML).toBe(`<!--teleport start--><!--teleport end-->`)
  898. expect(target.innerHTML).toBe(`<div><div>1</div><!--slot--></div>`)
  899. rerender(
  900. parentId,
  901. compileToFunction(`
  902. <Child>
  903. <template #default>
  904. <div>1</div>
  905. <div>2</div>
  906. </template>
  907. </Child>
  908. `),
  909. )
  910. expect(root.innerHTML).toBe(`<!--teleport start--><!--teleport end-->`)
  911. expect(target.innerHTML).toBe(
  912. `<div><div>1</div><div>2</div><!--slot--></div>`,
  913. )
  914. })
  915. test('rerender for component that has no active instance yet', () => {
  916. const id = 'no-active-instance-rerender'
  917. const Foo = {
  918. __vapor: true,
  919. __hmrId: id,
  920. render: () => template('foo')(),
  921. }
  922. createRecord(id, Foo)
  923. rerender(id, () => template('bar')())
  924. const root = document.createElement('div')
  925. define(Foo).create().mount(root)
  926. expect(root.innerHTML).toBe('bar')
  927. })
  928. test('reload for component that has no active instance yet', () => {
  929. const id = 'no-active-instance-reload'
  930. const Foo = {
  931. __vapor: true,
  932. __hmrId: id,
  933. render: () => template('foo')(),
  934. }
  935. createRecord(id, Foo)
  936. reload(id, {
  937. __hmrId: id,
  938. render: () => template('bar')(),
  939. })
  940. const root = document.createElement('div')
  941. define(Foo).render({}, root)
  942. expect(root.innerHTML).toBe('bar')
  943. })
  944. test('force update slot content change', () => {
  945. const root = document.createElement('div')
  946. const parentId = 'test-force-computed-parent'
  947. const childId = 'test-force-computed-child'
  948. const Child = {
  949. __vapor: true,
  950. __hmrId: childId,
  951. setup(_: any, { slots }: any) {
  952. const slotContent = computed(() => {
  953. return slots.default?.()
  954. })
  955. return { slotContent }
  956. },
  957. render: compileToFunction(`<component :is="() => slotContent" />`),
  958. }
  959. createRecord(childId, Child)
  960. const Parent = {
  961. __vapor: true,
  962. __hmrId: parentId,
  963. components: { Child },
  964. render: compileToFunction(`<Child>1</Child>`),
  965. }
  966. createRecord(parentId, Parent)
  967. // render(h(Parent), root)
  968. define(Parent).render({}, root)
  969. expect(root.innerHTML).toBe(`1<!--dynamic-component-->`)
  970. rerender(parentId, compileToFunction(`<Child>2</Child>`))
  971. expect(root.innerHTML).toBe(`2<!--dynamic-component-->`)
  972. })
  973. // #11248
  974. test('reload async component with multiple instances', async () => {
  975. const root = document.createElement('div')
  976. const childId = 'test-child-id'
  977. const Child = {
  978. __vapor: true,
  979. __hmrId: childId,
  980. setup() {
  981. const count = ref(0)
  982. return { count }
  983. },
  984. render: compileToFunction(`<div>{{ count }}</div>`),
  985. }
  986. const Comp = defineVaporAsyncComponent(() => Promise.resolve(Child))
  987. const appId = 'test-app-id'
  988. const App = {
  989. __hmrId: appId,
  990. render() {
  991. return [createComponent(Comp), createComponent(Comp)]
  992. },
  993. }
  994. createRecord(appId, App)
  995. define(App).render({}, root)
  996. await timeout()
  997. expect(root.innerHTML).toBe(
  998. `<div>0</div><!--async component--><div>0</div><!--async component-->`,
  999. )
  1000. // change count to 1
  1001. reload(childId, {
  1002. __vapor: true,
  1003. __hmrId: childId,
  1004. setup() {
  1005. const count = ref(1)
  1006. return { count }
  1007. },
  1008. render: compileToFunction(`<div>{{ count }}</div>`),
  1009. })
  1010. await timeout()
  1011. expect(root.innerHTML).toBe(
  1012. `<div>1</div><!--async component--><div>1</div><!--async component-->`,
  1013. )
  1014. })
  1015. test.todo('reload async child wrapped in Suspense + KeepAlive', async () => {
  1016. // const id = 'async-child-reload'
  1017. // const AsyncChild: ComponentOptions = {
  1018. // __hmrId: id,
  1019. // async setup() {
  1020. // await nextTick()
  1021. // return () => 'foo'
  1022. // },
  1023. // }
  1024. // createRecord(id, AsyncChild)
  1025. // const appId = 'test-app-id'
  1026. // const App: ComponentOptions = {
  1027. // __hmrId: appId,
  1028. // components: { AsyncChild },
  1029. // render: compileToFunction(`
  1030. // <div>
  1031. // <Suspense>
  1032. // <KeepAlive>
  1033. // <AsyncChild />
  1034. // </KeepAlive>
  1035. // </Suspense>
  1036. // </div>
  1037. // `),
  1038. // }
  1039. // const root = nodeOps.createElement('div')
  1040. // render(h(App), root)
  1041. // expect(serializeInner(root)).toBe('<div><!----></div>')
  1042. // await timeout()
  1043. // expect(serializeInner(root)).toBe('<div>foo</div>')
  1044. // reload(id, {
  1045. // __hmrId: id,
  1046. // async setup() {
  1047. // await nextTick()
  1048. // return () => 'bar'
  1049. // },
  1050. // })
  1051. // await timeout()
  1052. // expect(serializeInner(root)).toBe('<div>bar</div>')
  1053. })
  1054. test.todo('multi reload child wrapped in Suspense + KeepAlive', async () => {
  1055. // const id = 'test-child-reload-3'
  1056. // const Child: ComponentOptions = {
  1057. // __hmrId: id,
  1058. // setup() {
  1059. // const count = ref(0)
  1060. // return { count }
  1061. // },
  1062. // render: compileToFunction(`<div>{{ count }}</div>`),
  1063. // }
  1064. // createRecord(id, Child)
  1065. // const appId = 'test-app-id'
  1066. // const App: ComponentOptions = {
  1067. // __hmrId: appId,
  1068. // components: { Child },
  1069. // render: compileToFunction(`
  1070. // <KeepAlive>
  1071. // <Suspense>
  1072. // <Child />
  1073. // </Suspense>
  1074. // </KeepAlive>
  1075. // `),
  1076. // }
  1077. // const root = nodeOps.createElement('div')
  1078. // render(h(App), root)
  1079. // expect(serializeInner(root)).toBe('<div>0</div>')
  1080. // await timeout()
  1081. // reload(id, {
  1082. // __hmrId: id,
  1083. // setup() {
  1084. // const count = ref(1)
  1085. // return { count }
  1086. // },
  1087. // render: compileToFunction(`<div>{{ count }}</div>`),
  1088. // })
  1089. // await timeout()
  1090. // expect(serializeInner(root)).toBe('<div>1</div>')
  1091. // reload(id, {
  1092. // __hmrId: id,
  1093. // setup() {
  1094. // const count = ref(2)
  1095. // return { count }
  1096. // },
  1097. // render: compileToFunction(`<div>{{ count }}</div>`),
  1098. // })
  1099. // await timeout()
  1100. // expect(serializeInner(root)).toBe('<div>2</div>')
  1101. })
  1102. test('rerender for nested component', () => {
  1103. const id = 'child-nested-rerender'
  1104. const Foo = {
  1105. __vapor: true,
  1106. __hmrId: id,
  1107. setup(_ctx: any, { slots }: any) {
  1108. return slots.default()
  1109. },
  1110. }
  1111. createRecord(id, Foo)
  1112. const parentId = 'parent-nested-rerender'
  1113. const Parent = {
  1114. __vapor: true,
  1115. __hmrId: parentId,
  1116. render() {
  1117. return createComponent(
  1118. Foo,
  1119. {},
  1120. {
  1121. default: () => {
  1122. return createSlot('default')
  1123. },
  1124. },
  1125. )
  1126. },
  1127. }
  1128. const appId = 'app-nested-rerender'
  1129. const App = {
  1130. __vapor: true,
  1131. __hmrId: appId,
  1132. render: () =>
  1133. createComponent(
  1134. Parent,
  1135. {},
  1136. {
  1137. default: () => {
  1138. return createComponent(
  1139. Foo,
  1140. {},
  1141. {
  1142. default: () => template('foo')(),
  1143. },
  1144. )
  1145. },
  1146. },
  1147. ),
  1148. }
  1149. createRecord(parentId, App)
  1150. const root = document.createElement('div')
  1151. define(App).render({}, root)
  1152. expect(root.innerHTML).toBe('foo<!--slot-->')
  1153. rerender(id, () => template('bar')())
  1154. expect(root.innerHTML).toBe('bar')
  1155. })
  1156. test('reload nested components from single update', async () => {
  1157. const innerId = 'nested-reload-inner'
  1158. const outerId = 'nested-reload-outer'
  1159. let Inner = {
  1160. __vapor: true,
  1161. __hmrId: innerId,
  1162. render() {
  1163. return template('<div>foo</div>')()
  1164. },
  1165. }
  1166. let Outer = {
  1167. __vapor: true,
  1168. __hmrId: outerId,
  1169. render() {
  1170. return createComponent(Inner as any)
  1171. },
  1172. }
  1173. createRecord(innerId, Inner)
  1174. createRecord(outerId, Outer)
  1175. const App = {
  1176. __vapor: true,
  1177. render: () => createComponent(Outer),
  1178. }
  1179. const root = document.createElement('div')
  1180. define(App).render({}, root)
  1181. expect(root.innerHTML).toBe('<div>foo</div>')
  1182. Inner = {
  1183. __vapor: true,
  1184. __hmrId: innerId,
  1185. render() {
  1186. return template('<div>bar</div>')()
  1187. },
  1188. }
  1189. Outer = {
  1190. __vapor: true,
  1191. __hmrId: outerId,
  1192. render() {
  1193. return createComponent(Inner as any)
  1194. },
  1195. }
  1196. // trigger reload for both Outer and Inner
  1197. reload(outerId, Outer)
  1198. reload(innerId, Inner)
  1199. await nextTick()
  1200. expect(root.innerHTML).toBe('<div>bar</div>')
  1201. })
  1202. test('child reload + parent reload', async () => {
  1203. const root = document.createElement('div')
  1204. const childId = 'test1-child-reload'
  1205. const parentId = 'test1-parent-reload'
  1206. const { component: Child } = define({
  1207. __hmrId: childId,
  1208. setup() {
  1209. const msg = ref('child')
  1210. return { msg }
  1211. },
  1212. render: compileToFunction(`<div>{{ msg }}</div>`),
  1213. })
  1214. createRecord(childId, Child as any)
  1215. const { mount, component: Parent } = define({
  1216. __hmrId: parentId,
  1217. components: { Child },
  1218. setup() {
  1219. const msg = ref('root')
  1220. return { msg }
  1221. },
  1222. render: compileToFunction(`<Child/><div>{{ msg }}</div>`),
  1223. }).create()
  1224. createRecord(parentId, Parent as any)
  1225. mount(root)
  1226. expect(root.innerHTML).toMatchInlineSnapshot(
  1227. `"<div>child</div><div>root</div>"`,
  1228. )
  1229. // reload child
  1230. reload(childId, {
  1231. __hmrId: childId,
  1232. __vapor: true,
  1233. setup() {
  1234. const msg = ref('child changed')
  1235. return { msg }
  1236. },
  1237. render: compileToFunction(`<div>{{ msg }}</div>`),
  1238. })
  1239. expect(root.innerHTML).toMatchInlineSnapshot(
  1240. `"<div>child changed</div><div>root</div>"`,
  1241. )
  1242. // reload child again
  1243. reload(childId, {
  1244. __hmrId: childId,
  1245. __vapor: true,
  1246. setup() {
  1247. const msg = ref('child changed2')
  1248. return { msg }
  1249. },
  1250. render: compileToFunction(`<div>{{ msg }}</div>`),
  1251. })
  1252. expect(root.innerHTML).toMatchInlineSnapshot(
  1253. `"<div>child changed2</div><div>root</div>"`,
  1254. )
  1255. // reload parent
  1256. reload(parentId, {
  1257. __hmrId: parentId,
  1258. __vapor: true,
  1259. // @ts-expect-error
  1260. components: { Child },
  1261. setup() {
  1262. const msg = ref('root changed')
  1263. return { msg }
  1264. },
  1265. render: compileToFunction(`<Child/><div>{{ msg }}</div>`),
  1266. })
  1267. expect(root.innerHTML).toMatchInlineSnapshot(
  1268. `"<div>child changed2</div><div>root changed</div>"`,
  1269. )
  1270. })
  1271. test('child reload in dynamic branch should not break subsequent parent reload', async () => {
  1272. const root = document.createElement('div')
  1273. const childId = 'test-dynamic-child-reload'
  1274. const parentId = 'test-dynamic-parent-reload'
  1275. const Child = defineVaporComponent({
  1276. __hmrId: childId,
  1277. setup() {
  1278. const msg = ref('child')
  1279. return { msg }
  1280. },
  1281. render: compileToFunction(`<div>{{ msg }}</div>`),
  1282. })
  1283. createRecord(childId, Child as any)
  1284. const { mount, component: Parent } = define({
  1285. __hmrId: parentId,
  1286. components: { Child },
  1287. setup() {
  1288. const ok = ref(true)
  1289. return { ok }
  1290. },
  1291. render: compileToFunction(`<Child v-if="ok" />`),
  1292. }).create()
  1293. createRecord(parentId, Parent as any)
  1294. mount(root)
  1295. expect(root.innerHTML).toBe(`<div>child</div><!--if-->`)
  1296. reload(childId, {
  1297. __vapor: true,
  1298. __hmrId: childId,
  1299. setup() {
  1300. const msg = ref('child changed')
  1301. return { msg }
  1302. },
  1303. render: compileToFunction(`<div>{{ msg }}</div>`),
  1304. })
  1305. expect(root.innerHTML).toBe(`<div>child changed</div><!--if-->`)
  1306. reload(parentId, {
  1307. __vapor: true,
  1308. __hmrId: parentId,
  1309. components: { Child },
  1310. setup() {
  1311. const ok = ref(true)
  1312. return { ok }
  1313. },
  1314. render: compileToFunction(`<Child v-if="ok" />`),
  1315. })
  1316. await nextTick()
  1317. expect(root.innerHTML).toBe(`<div>child changed</div><!--if-->`)
  1318. })
  1319. test('child reload with multiple instances in dynamic branch should keep parent reload stable', async () => {
  1320. const root = document.createElement('div')
  1321. const childId = 'test-dynamic-multi-child-reload'
  1322. const parentId = 'test-dynamic-multi-parent-reload'
  1323. const Child = defineVaporComponent({
  1324. __hmrId: childId,
  1325. setup() {
  1326. const msg = ref('child')
  1327. return { msg }
  1328. },
  1329. render: compileToFunction(`<div>{{ msg }}</div>`),
  1330. })
  1331. createRecord(childId, Child as any)
  1332. const { mount, component: Parent } = define({
  1333. __hmrId: parentId,
  1334. components: { Child },
  1335. setup() {
  1336. const ok = ref(true)
  1337. return { ok }
  1338. },
  1339. render: compileToFunction(
  1340. `<template v-if="ok"><Child/><Child/></template>`,
  1341. ),
  1342. }).create()
  1343. createRecord(parentId, Parent as any)
  1344. mount(root)
  1345. expect(root.textContent).toBe(`childchild`)
  1346. reload(childId, {
  1347. __vapor: true,
  1348. __hmrId: childId,
  1349. setup() {
  1350. const msg = ref('child changed')
  1351. return { msg }
  1352. },
  1353. render: compileToFunction(`<div>{{ msg }}</div>`),
  1354. })
  1355. expect(root.textContent).toBe(`child changedchild changed`)
  1356. reload(parentId, {
  1357. __vapor: true,
  1358. __hmrId: parentId,
  1359. components: { Child },
  1360. setup() {
  1361. const ok = ref(true)
  1362. return { ok }
  1363. },
  1364. render: compileToFunction(
  1365. `<template v-if="ok"><Child/><Child/></template>`,
  1366. ),
  1367. })
  1368. await nextTick()
  1369. expect(root.textContent).toBe(`child changedchild changed`)
  1370. })
  1371. test('child reload in teleport dynamic branch should not break subsequent parent reload', async () => {
  1372. const root = document.createElement('div')
  1373. const target = document.createElement('div')
  1374. document.body.appendChild(root)
  1375. document.body.appendChild(target)
  1376. const childId = 'test-teleport-dynamic-child-reload'
  1377. const parentId = 'test-teleport-dynamic-parent-reload'
  1378. const Child = defineVaporComponent({
  1379. __hmrId: childId,
  1380. setup() {
  1381. const msg = ref('child')
  1382. return { msg }
  1383. },
  1384. render: compileToFunction(`<div>{{ msg }}</div>`),
  1385. })
  1386. createRecord(childId, Child as any)
  1387. const { mount, component: Parent } = define({
  1388. __hmrId: parentId,
  1389. components: { Child },
  1390. setup() {
  1391. const ok = ref(true)
  1392. return { ok, target }
  1393. },
  1394. render: compileToFunction(
  1395. `<teleport :to="target"><template v-if="ok"><Child/><span>sibling</span></template></teleport>`,
  1396. ),
  1397. }).create()
  1398. createRecord(parentId, Parent as any)
  1399. mount(root)
  1400. expect(target.textContent).toBe(`childsibling`)
  1401. reload(childId, {
  1402. __vapor: true,
  1403. __hmrId: childId,
  1404. setup() {
  1405. const msg = ref('child changed')
  1406. return { msg }
  1407. },
  1408. render: compileToFunction(`<div>{{ msg }}</div>`),
  1409. })
  1410. expect(target.textContent).toBe(`child changedsibling`)
  1411. reload(parentId, {
  1412. __vapor: true,
  1413. __hmrId: parentId,
  1414. components: { Child },
  1415. setup() {
  1416. const ok = ref(true)
  1417. return { ok, target }
  1418. },
  1419. render: compileToFunction(
  1420. `<teleport :to="target"><template v-if="ok"><Child/><span>sibling</span></template></teleport>`,
  1421. ),
  1422. })
  1423. await nextTick()
  1424. expect(target.textContent).toBe(`child changedsibling`)
  1425. })
  1426. // Vapor router-view has no render function (setup-only).
  1427. // When HMR rerender is triggered, the setup function is re-executed.
  1428. // Ensure provide() warning is suppressed.
  1429. test('rerender setup-only component', async () => {
  1430. const childId = 'test-child-reload-01'
  1431. const Child = defineVaporComponent({
  1432. __hmrId: childId,
  1433. render: compileToFunction(`<div>foo</div>`),
  1434. })
  1435. createRecord(childId, Child as any)
  1436. // without a render function
  1437. const Parent = defineVaporComponent({
  1438. setup() {
  1439. provide('foo', 'bar')
  1440. return createComponent(Child)
  1441. },
  1442. })
  1443. const { html } = define({
  1444. setup() {
  1445. return createComponent(Parent)
  1446. },
  1447. }).render()
  1448. expect(html()).toBe('<div>foo</div>')
  1449. // will trigger parent rerender
  1450. reload(childId, {
  1451. __hmrId: childId,
  1452. render: compileToFunction(`<div>bar</div>`),
  1453. })
  1454. await nextTick()
  1455. expect(html()).toBe('<div>bar</div>')
  1456. expect('provide() can only be used inside setup()').not.toHaveBeenWarned()
  1457. })
  1458. describe('switch vapor/vdom modes', () => {
  1459. test('reload vapor child under vdom parent should rerender parent', async () => {
  1460. const id = 'vapor-child-under-vdom-parent'
  1461. const Child = {
  1462. __vapor: true,
  1463. __hmrId: id,
  1464. render() {
  1465. return template('<div>foo</div>')()
  1466. },
  1467. }
  1468. createRecord(id, Child)
  1469. let parentRenderCount = 0
  1470. const Parent = {
  1471. render() {
  1472. parentRenderCount++
  1473. return h(Child as any)
  1474. },
  1475. }
  1476. const root = document.createElement('div')
  1477. const app = createApp(Parent)
  1478. app.use(vaporInteropPlugin)
  1479. app.mount(root)
  1480. expect(root.innerHTML).toBe('<div>foo</div>')
  1481. expect(parentRenderCount).toBe(1)
  1482. reload(id, {
  1483. __vapor: true,
  1484. __hmrId: id,
  1485. render() {
  1486. return template('<div>bar</div>')()
  1487. },
  1488. })
  1489. await nextTick()
  1490. expect(root.innerHTML).toBe('<div>bar</div>')
  1491. expect(parentRenderCount).toBe(2)
  1492. })
  1493. test('reload multiple vapor children under same vdom parent should rerender parent once', async () => {
  1494. const id = 'multiple-vapor-children-under-vdom-parent'
  1495. const Child = {
  1496. __vapor: true,
  1497. __hmrId: id,
  1498. render() {
  1499. return template('<div>foo</div>')()
  1500. },
  1501. }
  1502. createRecord(id, Child)
  1503. let parentRenderCount = 0
  1504. const Parent = {
  1505. render() {
  1506. parentRenderCount++
  1507. return [h(Child as any), h(Child as any)]
  1508. },
  1509. }
  1510. const root = document.createElement('div')
  1511. const app = createApp(Parent)
  1512. app.use(vaporInteropPlugin)
  1513. app.mount(root)
  1514. expect(root.innerHTML).toBe('<div>foo</div><div>foo</div>')
  1515. expect(parentRenderCount).toBe(1)
  1516. reload(id, {
  1517. __vapor: true,
  1518. __hmrId: id,
  1519. render() {
  1520. return template('<div>bar</div>')()
  1521. },
  1522. })
  1523. await nextTick()
  1524. expect(root.innerHTML).toBe('<div>bar</div><div>bar</div>')
  1525. expect(parentRenderCount).toBe(2)
  1526. })
  1527. test('vapor -> vdom', async () => {
  1528. const id = 'vapor-to-vdom'
  1529. const Comp = {
  1530. __vapor: true,
  1531. __hmrId: id,
  1532. render() {
  1533. return template('<div>foo</div>')()
  1534. },
  1535. }
  1536. createRecord(id, Comp)
  1537. const App = {
  1538. render() {
  1539. return h(Comp as any)
  1540. },
  1541. }
  1542. const root = document.createElement('div')
  1543. const app = createApp(App)
  1544. app.use(vaporInteropPlugin)
  1545. app.mount(root)
  1546. expect(root.innerHTML).toBe('<div>foo</div>')
  1547. // switch to vdom
  1548. reload(id, {
  1549. __hmrId: id,
  1550. render() {
  1551. return h('div', 'bar')
  1552. },
  1553. })
  1554. await nextTick()
  1555. expect(root.innerHTML).toBe('<div>bar</div>')
  1556. })
  1557. test('vdom -> vapor', async () => {
  1558. const id = 'vdom-to-vapor'
  1559. const Comp = {
  1560. __hmrId: id,
  1561. render() {
  1562. return h('div', 'foo')
  1563. },
  1564. }
  1565. createRecord(id, Comp)
  1566. const App = {
  1567. render() {
  1568. return h(Comp)
  1569. },
  1570. }
  1571. const root = document.createElement('div')
  1572. const app = createApp(App)
  1573. app.use(vaporInteropPlugin)
  1574. app.mount(root)
  1575. expect(root.innerHTML).toBe('<div>foo</div>')
  1576. // switch to vapor
  1577. reload(id, {
  1578. __vapor: true,
  1579. __hmrId: id,
  1580. render() {
  1581. return template('<div>bar</div>')()
  1582. },
  1583. })
  1584. await nextTick()
  1585. expect(root.innerHTML).toBe('<div>bar</div>')
  1586. })
  1587. })
  1588. })