hmr.spec.ts 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. import {
  2. type HMRRuntime,
  3. computed,
  4. nextTick,
  5. onActivated,
  6. onDeactivated,
  7. onMounted,
  8. onUnmounted,
  9. ref,
  10. toDisplayString,
  11. } from '@vue/runtime-dom'
  12. import { compileToVaporRender as compileToFunction, makeRender } from './_utils'
  13. import {
  14. createComponent,
  15. createSlot,
  16. createTemplateRefSetter,
  17. defineVaporAsyncComponent,
  18. defineVaporComponent,
  19. delegateEvents,
  20. renderEffect,
  21. setText,
  22. template,
  23. withVaporCtx,
  24. } from '@vue/runtime-vapor'
  25. import { BindingTypes } from '@vue/compiler-core'
  26. import type { VaporComponent } from '../src/component'
  27. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  28. const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
  29. const define = makeRender()
  30. const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
  31. const triggerEvent = (type: string, el: Element) => {
  32. const event = new Event(type, { bubbles: true })
  33. el.dispatchEvent(event)
  34. }
  35. delegateEvents('click')
  36. beforeEach(() => {
  37. document.body.innerHTML = ''
  38. })
  39. describe('hot module replacement', () => {
  40. test('inject global runtime', () => {
  41. expect(createRecord).toBeDefined()
  42. expect(rerender).toBeDefined()
  43. expect(reload).toBeDefined()
  44. })
  45. test('createRecord', () => {
  46. expect(createRecord('test1', {})).toBe(true)
  47. // if id has already been created, should return false
  48. expect(createRecord('test1', {})).toBe(false)
  49. })
  50. test('rerender', async () => {
  51. const root = document.createElement('div')
  52. const parentId = 'test2-parent'
  53. const childId = 'test2-child'
  54. document.body.appendChild(root)
  55. const Child = defineVaporComponent({
  56. __hmrId: childId,
  57. render: compileToFunction('<div><slot/></div>'),
  58. })
  59. createRecord(childId, Child as any)
  60. const Parent = defineVaporComponent({
  61. __hmrId: parentId,
  62. // @ts-expect-error ObjectVaporComponent doesn't have components
  63. components: { Child },
  64. setup() {
  65. const count = ref(0)
  66. return { count }
  67. },
  68. render: compileToFunction(
  69. `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`,
  70. ),
  71. })
  72. createRecord(parentId, Parent as any)
  73. const { mount } = define(Parent).create()
  74. mount(root)
  75. expect(root.innerHTML).toBe(`<div>0<div>0<!--slot--></div></div>`)
  76. // Perform some state change. This change should be preserved after the
  77. // re-render!
  78. // triggerEvent(root.children[0] as TestElement, 'click')
  79. triggerEvent('click', root.children[0])
  80. await nextTick()
  81. expect(root.innerHTML).toBe(`<div>1<div>1<!--slot--></div></div>`)
  82. // Update text while preserving state
  83. rerender(
  84. parentId,
  85. compileToFunction(
  86. `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`,
  87. ),
  88. )
  89. expect(root.innerHTML).toBe(`<div>1!<div>1<!--slot--></div></div>`)
  90. // Should force child update on slot content change
  91. rerender(
  92. parentId,
  93. compileToFunction(
  94. `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`,
  95. ),
  96. )
  97. expect(root.innerHTML).toBe(`<div>1!<div>1!<!--slot--></div></div>`)
  98. // Should force update element children despite block optimization
  99. rerender(
  100. parentId,
  101. compileToFunction(
  102. `<div @click="count++">{{ count }}<span>{{ count }}</span>
  103. <Child>{{ count }}!</Child>
  104. </div>`,
  105. ),
  106. )
  107. expect(root.innerHTML).toBe(
  108. `<div>1<span>1</span><div>1!<!--slot--></div></div>`,
  109. )
  110. // Should force update child slot elements
  111. rerender(
  112. parentId,
  113. compileToFunction(
  114. `<div @click="count++">
  115. <Child><span>{{ count }}</span></Child>
  116. </div>`,
  117. ),
  118. )
  119. expect(root.innerHTML).toBe(
  120. `<div><div><span>1</span><!--slot--></div></div>`,
  121. )
  122. })
  123. test('reload', async () => {
  124. const root = document.createElement('div')
  125. const childId = 'test3-child'
  126. const unmountSpy = vi.fn()
  127. const mountSpy = vi.fn()
  128. const Child = defineVaporComponent({
  129. __hmrId: childId,
  130. setup() {
  131. onUnmounted(unmountSpy)
  132. const count = ref(0)
  133. return { count }
  134. },
  135. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  136. })
  137. createRecord(childId, Child as any)
  138. const Parent = defineVaporComponent({
  139. __hmrId: 'parentId',
  140. render: () => createComponent(Child),
  141. })
  142. define(Parent).create().mount(root)
  143. expect(root.innerHTML).toBe(`<div>0</div>`)
  144. reload(childId, {
  145. __hmrId: childId,
  146. setup() {
  147. onMounted(mountSpy)
  148. const count = ref(1)
  149. return { count }
  150. },
  151. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  152. })
  153. await nextTick()
  154. expect(root.innerHTML).toBe(`<div>1</div>`)
  155. expect(unmountSpy).toHaveBeenCalledTimes(1)
  156. expect(mountSpy).toHaveBeenCalledTimes(1)
  157. })
  158. test('reload KeepAlive slot', async () => {
  159. const root = document.createElement('div')
  160. document.body.appendChild(root)
  161. const childId = 'test-child-keep-alive'
  162. const unmountSpy = vi.fn()
  163. const mountSpy = vi.fn()
  164. const activeSpy = vi.fn()
  165. const deactivatedSpy = vi.fn()
  166. const Child = defineVaporComponent({
  167. __hmrId: childId,
  168. setup() {
  169. onUnmounted(unmountSpy)
  170. const count = ref(0)
  171. return { count }
  172. },
  173. render: compileToFunction(`<div>{{ count }}</div>`),
  174. })
  175. createRecord(childId, Child as any)
  176. const Parent = defineVaporComponent({
  177. __hmrId: 'parentId',
  178. // @ts-expect-error
  179. components: { Child },
  180. setup() {
  181. const toggle = ref(true)
  182. return { toggle }
  183. },
  184. render: compileToFunction(
  185. `<button @click="toggle = !toggle" />
  186. <KeepAlive><Child v-if="toggle" /></KeepAlive>`,
  187. ),
  188. })
  189. define(Parent).create().mount(root)
  190. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  191. reload(childId, {
  192. __hmrId: childId,
  193. __vapor: true,
  194. setup() {
  195. onMounted(mountSpy)
  196. onUnmounted(unmountSpy)
  197. onActivated(activeSpy)
  198. onDeactivated(deactivatedSpy)
  199. const count = ref(1)
  200. return { count }
  201. },
  202. render: compileToFunction(`<div>{{ count }}</div>`),
  203. })
  204. await nextTick()
  205. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  206. expect(unmountSpy).toHaveBeenCalledTimes(1)
  207. expect(mountSpy).toHaveBeenCalledTimes(1)
  208. expect(activeSpy).toHaveBeenCalledTimes(1)
  209. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  210. // should not unmount when toggling
  211. triggerEvent('click', root.children[0] as Element)
  212. await nextTick()
  213. expect(unmountSpy).toHaveBeenCalledTimes(1)
  214. expect(mountSpy).toHaveBeenCalledTimes(1)
  215. expect(activeSpy).toHaveBeenCalledTimes(1)
  216. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  217. // should not mount when toggling
  218. triggerEvent('click', root.children[0] as Element)
  219. await nextTick()
  220. expect(unmountSpy).toHaveBeenCalledTimes(1)
  221. expect(mountSpy).toHaveBeenCalledTimes(1)
  222. expect(activeSpy).toHaveBeenCalledTimes(2)
  223. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  224. })
  225. test('reload KeepAlive slot in Transition', async () => {
  226. const root = document.createElement('div')
  227. document.body.appendChild(root)
  228. const childId = 'test-transition-keep-alive-reload'
  229. const unmountSpy = vi.fn()
  230. const mountSpy = vi.fn()
  231. const activeSpy = vi.fn()
  232. const deactivatedSpy = vi.fn()
  233. const Child = defineVaporComponent({
  234. __hmrId: childId,
  235. setup() {
  236. onUnmounted(unmountSpy)
  237. const count = ref(0)
  238. return { count }
  239. },
  240. render: compileToFunction(`<div>{{ count }}</div>`),
  241. })
  242. createRecord(childId, Child as any)
  243. const Parent = defineVaporComponent({
  244. __hmrId: 'parentId',
  245. // @ts-expect-error
  246. components: { Child },
  247. setup() {
  248. const toggle = ref(true)
  249. return { toggle }
  250. },
  251. render: compileToFunction(
  252. `<button @click="toggle = !toggle" />
  253. <Transition>
  254. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  255. </Transition>`,
  256. ),
  257. })
  258. define(Parent).create().mount(root)
  259. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  260. reload(childId, {
  261. __hmrId: childId,
  262. __vapor: true,
  263. setup() {
  264. onMounted(mountSpy)
  265. onUnmounted(unmountSpy)
  266. onActivated(activeSpy)
  267. onDeactivated(deactivatedSpy)
  268. const count = ref(1)
  269. return { count }
  270. },
  271. render: compileToFunction(`<div>{{ count }}</div>`),
  272. })
  273. await nextTick()
  274. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  275. expect(unmountSpy).toHaveBeenCalledTimes(1)
  276. expect(mountSpy).toHaveBeenCalledTimes(1)
  277. expect(activeSpy).toHaveBeenCalledTimes(1)
  278. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  279. // should not unmount when toggling
  280. triggerEvent('click', root.children[0] as Element)
  281. await nextTick()
  282. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  283. expect(unmountSpy).toHaveBeenCalledTimes(1)
  284. expect(mountSpy).toHaveBeenCalledTimes(1)
  285. expect(activeSpy).toHaveBeenCalledTimes(1)
  286. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  287. // should not mount when toggling
  288. triggerEvent('click', root.children[0] as Element)
  289. await nextTick()
  290. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  291. expect(unmountSpy).toHaveBeenCalledTimes(1)
  292. expect(mountSpy).toHaveBeenCalledTimes(1)
  293. expect(activeSpy).toHaveBeenCalledTimes(2)
  294. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  295. })
  296. test('reload KeepAlive slot in Transition with out-in', async () => {
  297. const root = document.createElement('div')
  298. document.body.appendChild(root)
  299. const childId = 'test-transition-keep-alive-reload-with-out-in'
  300. const unmountSpy = vi.fn()
  301. const mountSpy = vi.fn()
  302. const activeSpy = vi.fn()
  303. const deactivatedSpy = vi.fn()
  304. const Child = defineVaporComponent({
  305. name: 'original',
  306. __hmrId: childId,
  307. setup() {
  308. onUnmounted(unmountSpy)
  309. const count = ref(0)
  310. return { count }
  311. },
  312. render: compileToFunction(`<div>{{ count }}</div>`),
  313. })
  314. createRecord(childId, Child as any)
  315. const Parent = defineVaporComponent({
  316. // @ts-expect-error
  317. components: { Child },
  318. setup() {
  319. function onLeave(_: any, done: Function) {
  320. setTimeout(done, 0)
  321. }
  322. const toggle = ref(true)
  323. return { toggle, onLeave }
  324. },
  325. render: compileToFunction(
  326. `<button @click="toggle = !toggle" />
  327. <Transition mode="out-in" @leave="onLeave">
  328. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  329. </Transition>`,
  330. ),
  331. })
  332. define(Parent).create().mount(root)
  333. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  334. reload(childId, {
  335. name: 'updated',
  336. __hmrId: childId,
  337. __vapor: true,
  338. setup() {
  339. onMounted(mountSpy)
  340. onUnmounted(unmountSpy)
  341. onActivated(activeSpy)
  342. onDeactivated(deactivatedSpy)
  343. const count = ref(1)
  344. return { count }
  345. },
  346. render: compileToFunction(`<div>{{ count }}</div>`),
  347. })
  348. await nextTick()
  349. await new Promise(r => setTimeout(r, 0))
  350. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  351. expect(unmountSpy).toHaveBeenCalledTimes(1)
  352. expect(mountSpy).toHaveBeenCalledTimes(1)
  353. expect(activeSpy).toHaveBeenCalledTimes(1)
  354. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  355. // should not unmount when toggling
  356. triggerEvent('click', root.children[0] as Element)
  357. await nextTick()
  358. await new Promise(r => setTimeout(r, 0))
  359. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  360. expect(unmountSpy).toHaveBeenCalledTimes(1)
  361. expect(mountSpy).toHaveBeenCalledTimes(1)
  362. expect(activeSpy).toHaveBeenCalledTimes(1)
  363. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  364. // should not mount when toggling
  365. triggerEvent('click', root.children[0] as Element)
  366. await nextTick()
  367. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  368. expect(unmountSpy).toHaveBeenCalledTimes(1)
  369. expect(mountSpy).toHaveBeenCalledTimes(1)
  370. expect(activeSpy).toHaveBeenCalledTimes(2)
  371. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  372. })
  373. // TODO: renderEffect not re-run after child reload
  374. // it requires parent rerender to align with vdom
  375. test.todo('reload: avoid infinite recursion', async () => {
  376. const root = document.createElement('div')
  377. document.body.appendChild(root)
  378. const childId = 'test-child-6930'
  379. const unmountSpy = vi.fn()
  380. const mountSpy = vi.fn()
  381. const Child = defineVaporComponent({
  382. __hmrId: childId,
  383. setup(_, { expose }) {
  384. const count = ref(0)
  385. expose({
  386. count,
  387. })
  388. onUnmounted(unmountSpy)
  389. return { count }
  390. },
  391. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  392. })
  393. createRecord(childId, Child as any)
  394. const Parent = defineVaporComponent({
  395. setup() {
  396. const com1 = ref()
  397. const changeRef1 = (value: any) => (com1.value = value)
  398. const com2 = ref()
  399. const changeRef2 = (value: any) => (com2.value = value)
  400. const setRef = createTemplateRefSetter()
  401. const n0 = createComponent(Child)
  402. setRef(n0, changeRef1)
  403. const n1 = createComponent(Child)
  404. setRef(n1, changeRef2)
  405. const n2 = template(' ')() as any
  406. renderEffect(() => {
  407. setText(n2, toDisplayString(com1.value.count))
  408. })
  409. return [n0, n1, n2]
  410. },
  411. })
  412. define(Parent).create().mount(root)
  413. await nextTick()
  414. expect(root.innerHTML).toBe(`<div>0</div><div>0</div>0`)
  415. reload(childId, {
  416. __hmrId: childId,
  417. __vapor: true,
  418. setup() {
  419. onMounted(mountSpy)
  420. const count = ref(1)
  421. return { count }
  422. },
  423. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  424. })
  425. await nextTick()
  426. expect(root.innerHTML).toBe(`<div>1</div><div>1</div>1`)
  427. expect(unmountSpy).toHaveBeenCalledTimes(2)
  428. expect(mountSpy).toHaveBeenCalledTimes(2)
  429. })
  430. test('static el reference', async () => {
  431. const root = document.createElement('div')
  432. document.body.appendChild(root)
  433. const id = 'test-static-el'
  434. const template = `<div>
  435. <div>{{ count }}</div>
  436. <button @click="count++">++</button>
  437. </div>`
  438. const Comp = defineVaporComponent({
  439. __hmrId: id,
  440. setup() {
  441. const count = ref(0)
  442. return { count }
  443. },
  444. render: compileToFunction(template),
  445. })
  446. createRecord(id, Comp as any)
  447. define(Comp).create().mount(root)
  448. expect(root.innerHTML).toBe(`<div><div>0</div><button>++</button></div>`)
  449. // 1. click to trigger update
  450. triggerEvent('click', root.children[0].children[1] as Element)
  451. await nextTick()
  452. expect(root.innerHTML).toBe(`<div><div>1</div><button>++</button></div>`)
  453. // 2. trigger HMR
  454. rerender(
  455. id,
  456. compileToFunction(template.replace(`<button`, `<button class="foo"`)),
  457. )
  458. expect(root.innerHTML).toBe(
  459. `<div><div>1</div><button class="foo">++</button></div>`,
  460. )
  461. })
  462. test('force update child component w/ static props', () => {
  463. const root = document.createElement('div')
  464. const parentId = 'test-force-props-parent'
  465. const childId = 'test-force-props-child'
  466. const Child = defineVaporComponent({
  467. __hmrId: childId,
  468. props: {
  469. msg: String,
  470. },
  471. render: compileToFunction(`<div>{{ msg }}</div>`, {
  472. bindingMetadata: {
  473. msg: BindingTypes.PROPS,
  474. },
  475. }),
  476. })
  477. createRecord(childId, Child as any)
  478. const Parent = defineVaporComponent({
  479. __hmrId: parentId,
  480. // @ts-expect-error
  481. components: { Child },
  482. render: compileToFunction(`<Child msg="foo" />`),
  483. })
  484. createRecord(parentId, Parent as any)
  485. define(Parent).create().mount(root)
  486. expect(root.innerHTML).toBe(`<div>foo</div>`)
  487. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  488. expect(root.innerHTML).toBe(`<div>bar</div>`)
  489. })
  490. test('remove static class from parent', () => {
  491. const root = document.createElement('div')
  492. const parentId = 'test-force-class-parent'
  493. const childId = 'test-force-class-child'
  494. const Child = defineVaporComponent({
  495. __hmrId: childId,
  496. render: compileToFunction(`<div>child</div>`),
  497. })
  498. createRecord(childId, Child as any)
  499. const Parent = defineVaporComponent({
  500. __hmrId: parentId,
  501. // @ts-expect-error
  502. components: { Child },
  503. render: compileToFunction(`<Child class="test" />`),
  504. })
  505. createRecord(parentId, Parent as any)
  506. define(Parent).create().mount(root)
  507. expect(root.innerHTML).toBe(`<div class="test">child</div>`)
  508. rerender(parentId, compileToFunction(`<Child/>`))
  509. expect(root.innerHTML).toBe(`<div>child</div>`)
  510. })
  511. test('rerender if any parent in the parent chain', () => {
  512. const root = document.createElement('div')
  513. const parent = 'test-force-props-parent-'
  514. const childId = 'test-force-props-child'
  515. const numberOfParents = 5
  516. const Child = defineVaporComponent({
  517. __hmrId: childId,
  518. render: compileToFunction(`<div>child</div>`),
  519. })
  520. createRecord(childId, Child as any)
  521. const components: VaporComponent[] = []
  522. for (let i = 0; i < numberOfParents; i++) {
  523. const parentId = `${parent}${i}`
  524. const parentComp: VaporComponent = {
  525. __vapor: true,
  526. __hmrId: parentId,
  527. }
  528. components.push(parentComp)
  529. if (i === 0) {
  530. parentComp.render = compileToFunction(`<Child />`)
  531. // @ts-expect-error
  532. parentComp.components = {
  533. Child,
  534. }
  535. } else {
  536. parentComp.render = compileToFunction(`<Parent />`)
  537. // @ts-expect-error
  538. parentComp.components = {
  539. Parent: components[i - 1],
  540. }
  541. }
  542. createRecord(parentId, parentComp as any)
  543. }
  544. const last = components[components.length - 1]
  545. define(last).create().mount(root)
  546. expect(root.innerHTML).toBe(`<div>child</div>`)
  547. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  548. expect(root.innerHTML).toBe(`<div class="test">child</div>`)
  549. })
  550. test('rerender with Teleport', () => {
  551. const root = document.createElement('div')
  552. const target = document.createElement('div')
  553. document.body.appendChild(root)
  554. document.body.appendChild(target)
  555. const parentId = 'parent-teleport'
  556. const Child = defineVaporComponent({
  557. setup() {
  558. return { target }
  559. },
  560. render: compileToFunction(`
  561. <teleport :to="target">
  562. <div>
  563. <slot/>
  564. </div>
  565. </teleport>
  566. `),
  567. })
  568. const Parent = {
  569. __vapor: true,
  570. __hmrId: parentId,
  571. components: { Child },
  572. render: compileToFunction(`
  573. <Child>
  574. <template #default>
  575. <div>1</div>
  576. </template>
  577. </Child>
  578. `),
  579. }
  580. createRecord(parentId, Parent as any)
  581. define(Parent).create().mount(root)
  582. expect(root.innerHTML).toBe(`<!--teleport start--><!--teleport end-->`)
  583. expect(target.innerHTML).toBe(`<div><div>1</div><!--slot--></div>`)
  584. rerender(
  585. parentId,
  586. compileToFunction(`
  587. <Child>
  588. <template #default>
  589. <div>1</div>
  590. <div>2</div>
  591. </template>
  592. </Child>
  593. `),
  594. )
  595. expect(root.innerHTML).toBe(`<!--teleport start--><!--teleport end-->`)
  596. expect(target.innerHTML).toBe(
  597. `<div><div>1</div><div>2</div><!--slot--></div>`,
  598. )
  599. })
  600. test('rerender for component that has no active instance yet', () => {
  601. const id = 'no-active-instance-rerender'
  602. const Foo = {
  603. __vapor: true,
  604. __hmrId: id,
  605. render: () => template('foo')(),
  606. }
  607. createRecord(id, Foo)
  608. rerender(id, () => template('bar')())
  609. const root = document.createElement('div')
  610. define(Foo).create().mount(root)
  611. expect(root.innerHTML).toBe('bar')
  612. })
  613. test('reload for component that has no active instance yet', () => {
  614. const id = 'no-active-instance-reload'
  615. const Foo = {
  616. __vapor: true,
  617. __hmrId: id,
  618. render: () => template('foo')(),
  619. }
  620. createRecord(id, Foo)
  621. reload(id, {
  622. __hmrId: id,
  623. render: () => template('bar')(),
  624. })
  625. const root = document.createElement('div')
  626. define(Foo).render({}, root)
  627. expect(root.innerHTML).toBe('bar')
  628. })
  629. test('force update slot content change', () => {
  630. const root = document.createElement('div')
  631. const parentId = 'test-force-computed-parent'
  632. const childId = 'test-force-computed-child'
  633. const Child = {
  634. __vapor: true,
  635. __hmrId: childId,
  636. setup(_: any, { slots }: any) {
  637. const slotContent = computed(() => {
  638. return slots.default?.()
  639. })
  640. return { slotContent }
  641. },
  642. render: compileToFunction(`<component :is="() => slotContent" />`),
  643. }
  644. createRecord(childId, Child)
  645. const Parent = {
  646. __vapor: true,
  647. __hmrId: parentId,
  648. components: { Child },
  649. render: compileToFunction(`<Child>1</Child>`),
  650. }
  651. createRecord(parentId, Parent)
  652. // render(h(Parent), root)
  653. define(Parent).render({}, root)
  654. expect(root.innerHTML).toBe(`1<!--dynamic-component-->`)
  655. rerender(parentId, compileToFunction(`<Child>2</Child>`))
  656. expect(root.innerHTML).toBe(`2<!--dynamic-component-->`)
  657. })
  658. // #11248
  659. test('reload async component with multiple instances', async () => {
  660. const root = document.createElement('div')
  661. const childId = 'test-child-id'
  662. const Child = {
  663. __vapor: true,
  664. __hmrId: childId,
  665. setup() {
  666. const count = ref(0)
  667. return { count }
  668. },
  669. render: compileToFunction(`<div>{{ count }}</div>`),
  670. }
  671. const Comp = defineVaporAsyncComponent(() => Promise.resolve(Child))
  672. const appId = 'test-app-id'
  673. const App = {
  674. __hmrId: appId,
  675. render() {
  676. return [createComponent(Comp), createComponent(Comp)]
  677. },
  678. }
  679. createRecord(appId, App)
  680. define(App).render({}, root)
  681. await timeout()
  682. expect(root.innerHTML).toBe(
  683. `<div>0</div><!--async component--><div>0</div><!--async component-->`,
  684. )
  685. // change count to 1
  686. reload(childId, {
  687. __vapor: true,
  688. __hmrId: childId,
  689. setup() {
  690. const count = ref(1)
  691. return { count }
  692. },
  693. render: compileToFunction(`<div>{{ count }}</div>`),
  694. })
  695. await timeout()
  696. expect(root.innerHTML).toBe(
  697. `<div>1</div><!--async component--><div>1</div><!--async component-->`,
  698. )
  699. })
  700. test.todo('reload async child wrapped in Suspense + KeepAlive', async () => {
  701. // const id = 'async-child-reload'
  702. // const AsyncChild: ComponentOptions = {
  703. // __hmrId: id,
  704. // async setup() {
  705. // await nextTick()
  706. // return () => 'foo'
  707. // },
  708. // }
  709. // createRecord(id, AsyncChild)
  710. // const appId = 'test-app-id'
  711. // const App: ComponentOptions = {
  712. // __hmrId: appId,
  713. // components: { AsyncChild },
  714. // render: compileToFunction(`
  715. // <div>
  716. // <Suspense>
  717. // <KeepAlive>
  718. // <AsyncChild />
  719. // </KeepAlive>
  720. // </Suspense>
  721. // </div>
  722. // `),
  723. // }
  724. // const root = nodeOps.createElement('div')
  725. // render(h(App), root)
  726. // expect(serializeInner(root)).toBe('<div><!----></div>')
  727. // await timeout()
  728. // expect(serializeInner(root)).toBe('<div>foo</div>')
  729. // reload(id, {
  730. // __hmrId: id,
  731. // async setup() {
  732. // await nextTick()
  733. // return () => 'bar'
  734. // },
  735. // })
  736. // await timeout()
  737. // expect(serializeInner(root)).toBe('<div>bar</div>')
  738. })
  739. test.todo('multi reload child wrapped in Suspense + KeepAlive', async () => {
  740. // const id = 'test-child-reload-3'
  741. // const Child: ComponentOptions = {
  742. // __hmrId: id,
  743. // setup() {
  744. // const count = ref(0)
  745. // return { count }
  746. // },
  747. // render: compileToFunction(`<div>{{ count }}</div>`),
  748. // }
  749. // createRecord(id, Child)
  750. // const appId = 'test-app-id'
  751. // const App: ComponentOptions = {
  752. // __hmrId: appId,
  753. // components: { Child },
  754. // render: compileToFunction(`
  755. // <KeepAlive>
  756. // <Suspense>
  757. // <Child />
  758. // </Suspense>
  759. // </KeepAlive>
  760. // `),
  761. // }
  762. // const root = nodeOps.createElement('div')
  763. // render(h(App), root)
  764. // expect(serializeInner(root)).toBe('<div>0</div>')
  765. // await timeout()
  766. // reload(id, {
  767. // __hmrId: id,
  768. // setup() {
  769. // const count = ref(1)
  770. // return { count }
  771. // },
  772. // render: compileToFunction(`<div>{{ count }}</div>`),
  773. // })
  774. // await timeout()
  775. // expect(serializeInner(root)).toBe('<div>1</div>')
  776. // reload(id, {
  777. // __hmrId: id,
  778. // setup() {
  779. // const count = ref(2)
  780. // return { count }
  781. // },
  782. // render: compileToFunction(`<div>{{ count }}</div>`),
  783. // })
  784. // await timeout()
  785. // expect(serializeInner(root)).toBe('<div>2</div>')
  786. })
  787. test('rerender for nested component', () => {
  788. const id = 'child-nested-rerender'
  789. const Foo = {
  790. __vapor: true,
  791. __hmrId: id,
  792. setup(_ctx: any, { slots }: any) {
  793. return slots.default()
  794. },
  795. }
  796. createRecord(id, Foo)
  797. const parentId = 'parent-nested-rerender'
  798. const Parent = {
  799. __vapor: true,
  800. __hmrId: parentId,
  801. render() {
  802. return createComponent(
  803. Foo,
  804. {},
  805. {
  806. default: withVaporCtx(() => {
  807. return createSlot('default')
  808. }),
  809. },
  810. )
  811. },
  812. }
  813. const appId = 'app-nested-rerender'
  814. const App = {
  815. __vapor: true,
  816. __hmrId: appId,
  817. render: () =>
  818. createComponent(
  819. Parent,
  820. {},
  821. {
  822. default: withVaporCtx(() => {
  823. return createComponent(
  824. Foo,
  825. {},
  826. {
  827. default: () => template('foo')(),
  828. },
  829. )
  830. }),
  831. },
  832. ),
  833. }
  834. createRecord(parentId, App)
  835. const root = document.createElement('div')
  836. define(App).render({}, root)
  837. expect(root.innerHTML).toBe('foo<!--slot-->')
  838. rerender(id, () => template('bar')())
  839. expect(root.innerHTML).toBe('bar')
  840. })
  841. test('reload nested components from single update', async () => {
  842. const innerId = 'nested-reload-inner'
  843. const outerId = 'nested-reload-outer'
  844. let Inner = {
  845. __vapor: true,
  846. __hmrId: innerId,
  847. render() {
  848. return template('<div>foo</div>')()
  849. },
  850. }
  851. let Outer = {
  852. __vapor: true,
  853. __hmrId: outerId,
  854. render() {
  855. return createComponent(Inner as any)
  856. },
  857. }
  858. createRecord(innerId, Inner)
  859. createRecord(outerId, Outer)
  860. const App = {
  861. __vapor: true,
  862. render: () => createComponent(Outer),
  863. }
  864. const root = document.createElement('div')
  865. define(App).render({}, root)
  866. expect(root.innerHTML).toBe('<div>foo</div>')
  867. Inner = {
  868. __vapor: true,
  869. __hmrId: innerId,
  870. render() {
  871. return template('<div>bar</div>')()
  872. },
  873. }
  874. Outer = {
  875. __vapor: true,
  876. __hmrId: outerId,
  877. render() {
  878. return createComponent(Inner as any)
  879. },
  880. }
  881. // trigger reload for both Outer and Inner
  882. reload(outerId, Outer)
  883. reload(innerId, Inner)
  884. await nextTick()
  885. expect(root.innerHTML).toBe('<div>bar</div>')
  886. })
  887. test('child reload + parent reload', async () => {
  888. const root = document.createElement('div')
  889. const childId = 'test1-child-reload'
  890. const parentId = 'test1-parent-reload'
  891. const { component: Child } = define({
  892. __hmrId: childId,
  893. setup() {
  894. const msg = ref('child')
  895. return { msg }
  896. },
  897. render: compileToFunction(`<div>{{ msg }}</div>`),
  898. })
  899. createRecord(childId, Child as any)
  900. const { mount, component: Parent } = define({
  901. __hmrId: parentId,
  902. // @ts-expect-error
  903. components: { Child },
  904. setup() {
  905. const msg = ref('root')
  906. return { msg }
  907. },
  908. render: compileToFunction(`<Child/><div>{{ msg }}</div>`),
  909. }).create()
  910. createRecord(parentId, Parent as any)
  911. mount(root)
  912. expect(root.innerHTML).toMatchInlineSnapshot(
  913. `"<div>child</div><div>root</div>"`,
  914. )
  915. // reload child
  916. reload(childId, {
  917. __hmrId: childId,
  918. __vapor: true,
  919. setup() {
  920. const msg = ref('child changed')
  921. return { msg }
  922. },
  923. render: compileToFunction(`<div>{{ msg }}</div>`),
  924. })
  925. expect(root.innerHTML).toMatchInlineSnapshot(
  926. `"<div>child changed</div><div>root</div>"`,
  927. )
  928. // reload child again
  929. reload(childId, {
  930. __hmrId: childId,
  931. __vapor: true,
  932. setup() {
  933. const msg = ref('child changed2')
  934. return { msg }
  935. },
  936. render: compileToFunction(`<div>{{ msg }}</div>`),
  937. })
  938. expect(root.innerHTML).toMatchInlineSnapshot(
  939. `"<div>child changed2</div><div>root</div>"`,
  940. )
  941. // reload parent
  942. reload(parentId, {
  943. __hmrId: parentId,
  944. __vapor: true,
  945. // @ts-expect-error
  946. components: { Child },
  947. setup() {
  948. const msg = ref('root changed')
  949. return { msg }
  950. },
  951. render: compileToFunction(`<Child/><div>{{ msg }}</div>`),
  952. })
  953. expect(root.innerHTML).toMatchInlineSnapshot(
  954. `"<div>child changed2</div><div>root changed</div>"`,
  955. )
  956. })
  957. })