hmr.spec.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. import {
  2. type HMRRuntime,
  3. computed,
  4. createApp,
  5. h,
  6. nextTick,
  7. onActivated,
  8. onDeactivated,
  9. onMounted,
  10. onUnmounted,
  11. provide,
  12. ref,
  13. toDisplayString,
  14. } from '@vue/runtime-dom'
  15. import { compileToVaporRender as compileToFunction, makeRender } from './_utils'
  16. import {
  17. createComponent,
  18. createSlot,
  19. createTemplateRefSetter,
  20. defineVaporAsyncComponent,
  21. defineVaporComponent,
  22. delegateEvents,
  23. renderEffect,
  24. setText,
  25. template,
  26. vaporInteropPlugin,
  27. withVaporCtx,
  28. } from '@vue/runtime-vapor'
  29. import { BindingTypes } from '@vue/compiler-core'
  30. import type { VaporComponent } from '../src/component'
  31. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  32. const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
  33. const define = makeRender()
  34. const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
  35. const triggerEvent = (type: string, el: Element) => {
  36. const event = new Event(type, { bubbles: true })
  37. el.dispatchEvent(event)
  38. }
  39. delegateEvents('click')
  40. beforeEach(() => {
  41. document.body.innerHTML = ''
  42. })
  43. describe('hot module replacement', () => {
  44. test('inject global runtime', () => {
  45. expect(createRecord).toBeDefined()
  46. expect(rerender).toBeDefined()
  47. expect(reload).toBeDefined()
  48. })
  49. test('createRecord', () => {
  50. expect(createRecord('test1', {})).toBe(true)
  51. // if id has already been created, should return false
  52. expect(createRecord('test1', {})).toBe(false)
  53. })
  54. test('rerender', async () => {
  55. const root = document.createElement('div')
  56. const parentId = 'test2-parent'
  57. const childId = 'test2-child'
  58. document.body.appendChild(root)
  59. const Child = defineVaporComponent({
  60. __hmrId: childId,
  61. render: compileToFunction('<div><slot/></div>'),
  62. })
  63. createRecord(childId, Child as any)
  64. const Parent = defineVaporComponent({
  65. __hmrId: parentId,
  66. components: { Child },
  67. setup() {
  68. const count = ref(0)
  69. return { count }
  70. },
  71. render: compileToFunction(
  72. `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`,
  73. ),
  74. })
  75. createRecord(parentId, Parent as any)
  76. const { mount } = define(Parent).create()
  77. mount(root)
  78. expect(root.innerHTML).toBe(`<div>0<div>0<!--slot--></div></div>`)
  79. // Perform some state change. This change should be preserved after the
  80. // re-render!
  81. // triggerEvent(root.children[0] as TestElement, 'click')
  82. triggerEvent('click', root.children[0])
  83. await nextTick()
  84. expect(root.innerHTML).toBe(`<div>1<div>1<!--slot--></div></div>`)
  85. // Update text while preserving state
  86. rerender(
  87. parentId,
  88. compileToFunction(
  89. `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`,
  90. ),
  91. )
  92. expect(root.innerHTML).toBe(`<div>1!<div>1<!--slot--></div></div>`)
  93. // Should force child update on slot content change
  94. rerender(
  95. parentId,
  96. compileToFunction(
  97. `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`,
  98. ),
  99. )
  100. expect(root.innerHTML).toBe(`<div>1!<div>1!<!--slot--></div></div>`)
  101. // Should force update element children despite block optimization
  102. rerender(
  103. parentId,
  104. compileToFunction(
  105. `<div @click="count++">{{ count }}<span>{{ count }}</span>
  106. <Child>{{ count }}!</Child>
  107. </div>`,
  108. ),
  109. )
  110. expect(root.innerHTML).toBe(
  111. `<div>1<span>1</span><div>1!<!--slot--></div></div>`,
  112. )
  113. // Should force update child slot elements
  114. rerender(
  115. parentId,
  116. compileToFunction(
  117. `<div @click="count++">
  118. <Child><span>{{ count }}</span></Child>
  119. </div>`,
  120. ),
  121. )
  122. expect(root.innerHTML).toBe(
  123. `<div><div><span>1</span><!--slot--></div></div>`,
  124. )
  125. })
  126. test('reload', async () => {
  127. const root = document.createElement('div')
  128. const childId = 'test3-child'
  129. const unmountSpy = vi.fn()
  130. const mountSpy = vi.fn()
  131. const Child = defineVaporComponent({
  132. __hmrId: childId,
  133. setup() {
  134. onUnmounted(unmountSpy)
  135. const count = ref(0)
  136. return { count }
  137. },
  138. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  139. })
  140. createRecord(childId, Child as any)
  141. const Parent = defineVaporComponent({
  142. __hmrId: 'parentId',
  143. render: () => createComponent(Child),
  144. })
  145. define(Parent).create().mount(root)
  146. expect(root.innerHTML).toBe(`<div>0</div>`)
  147. reload(childId, {
  148. __hmrId: childId,
  149. setup() {
  150. onMounted(mountSpy)
  151. const count = ref(1)
  152. return { count }
  153. },
  154. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  155. })
  156. await nextTick()
  157. expect(root.innerHTML).toBe(`<div>1</div>`)
  158. expect(unmountSpy).toHaveBeenCalledTimes(1)
  159. expect(mountSpy).toHaveBeenCalledTimes(1)
  160. })
  161. test('reload KeepAlive slot', async () => {
  162. const root = document.createElement('div')
  163. document.body.appendChild(root)
  164. const childId = 'test-child-keep-alive'
  165. const unmountSpy = vi.fn()
  166. const mountSpy = vi.fn()
  167. const activeSpy = vi.fn()
  168. const deactivatedSpy = vi.fn()
  169. const Child = defineVaporComponent({
  170. __hmrId: childId,
  171. setup() {
  172. onUnmounted(unmountSpy)
  173. const count = ref(0)
  174. return { count }
  175. },
  176. render: compileToFunction(`<div>{{ count }}</div>`),
  177. })
  178. createRecord(childId, Child as any)
  179. const Parent = defineVaporComponent({
  180. __hmrId: 'parentId',
  181. components: { Child },
  182. setup() {
  183. const toggle = ref(true)
  184. return { toggle }
  185. },
  186. render: compileToFunction(
  187. `<button @click="toggle = !toggle" />
  188. <KeepAlive><Child v-if="toggle" /></KeepAlive>`,
  189. ),
  190. })
  191. define(Parent).create().mount(root)
  192. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  193. reload(childId, {
  194. __hmrId: childId,
  195. __vapor: true,
  196. setup() {
  197. onMounted(mountSpy)
  198. onUnmounted(unmountSpy)
  199. onActivated(activeSpy)
  200. onDeactivated(deactivatedSpy)
  201. const count = ref(1)
  202. return { count }
  203. },
  204. render: compileToFunction(`<div>{{ count }}</div>`),
  205. })
  206. await nextTick()
  207. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  208. expect(unmountSpy).toHaveBeenCalledTimes(1)
  209. expect(mountSpy).toHaveBeenCalledTimes(1)
  210. expect(activeSpy).toHaveBeenCalledTimes(1)
  211. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  212. // should not unmount when toggling
  213. triggerEvent('click', root.children[0] as Element)
  214. await nextTick()
  215. expect(unmountSpy).toHaveBeenCalledTimes(1)
  216. expect(mountSpy).toHaveBeenCalledTimes(1)
  217. expect(activeSpy).toHaveBeenCalledTimes(1)
  218. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  219. // should not mount when toggling
  220. triggerEvent('click', root.children[0] as Element)
  221. await nextTick()
  222. expect(unmountSpy).toHaveBeenCalledTimes(1)
  223. expect(mountSpy).toHaveBeenCalledTimes(1)
  224. expect(activeSpy).toHaveBeenCalledTimes(2)
  225. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  226. })
  227. test('reload KeepAlive slot in Transition', async () => {
  228. const root = document.createElement('div')
  229. document.body.appendChild(root)
  230. const childId = 'test-transition-keep-alive-reload'
  231. const unmountSpy = vi.fn()
  232. const mountSpy = vi.fn()
  233. const activeSpy = vi.fn()
  234. const deactivatedSpy = vi.fn()
  235. const Child = defineVaporComponent({
  236. __hmrId: childId,
  237. setup() {
  238. onUnmounted(unmountSpy)
  239. const count = ref(0)
  240. return { count }
  241. },
  242. render: compileToFunction(`<div>{{ count }}</div>`),
  243. })
  244. createRecord(childId, Child as any)
  245. const Parent = defineVaporComponent({
  246. __hmrId: 'parentId',
  247. components: { Child },
  248. setup() {
  249. const toggle = ref(true)
  250. function onLeave(_: any, done: Function) {
  251. setTimeout(done, 0)
  252. }
  253. return { toggle, onLeave }
  254. },
  255. render: compileToFunction(
  256. `<button @click="toggle = !toggle" />
  257. <Transition @leave="onLeave">
  258. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  259. </Transition>`,
  260. ),
  261. })
  262. define(Parent).create().mount(root)
  263. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  264. reload(childId, {
  265. __hmrId: childId,
  266. __vapor: true,
  267. setup() {
  268. onMounted(mountSpy)
  269. onUnmounted(unmountSpy)
  270. onActivated(activeSpy)
  271. onDeactivated(deactivatedSpy)
  272. const count = ref(1)
  273. return { count }
  274. },
  275. render: compileToFunction(`<div>{{ count }}</div>`),
  276. })
  277. await nextTick()
  278. await new Promise(r => setTimeout(r, 0))
  279. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  280. expect(unmountSpy).toHaveBeenCalledTimes(1)
  281. expect(mountSpy).toHaveBeenCalledTimes(1)
  282. expect(activeSpy).toHaveBeenCalledTimes(1)
  283. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  284. // should not unmount when toggling
  285. triggerEvent('click', root.children[0] as Element)
  286. await nextTick()
  287. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  288. expect(unmountSpy).toHaveBeenCalledTimes(1)
  289. expect(mountSpy).toHaveBeenCalledTimes(1)
  290. expect(activeSpy).toHaveBeenCalledTimes(1)
  291. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  292. // should not mount when toggling
  293. triggerEvent('click', root.children[0] as Element)
  294. await nextTick()
  295. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  296. expect(unmountSpy).toHaveBeenCalledTimes(1)
  297. expect(mountSpy).toHaveBeenCalledTimes(1)
  298. expect(activeSpy).toHaveBeenCalledTimes(2)
  299. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  300. })
  301. test('reload KeepAlive slot in Transition with out-in', async () => {
  302. const root = document.createElement('div')
  303. document.body.appendChild(root)
  304. const childId = 'test-transition-keep-alive-reload-with-out-in'
  305. const unmountSpy = vi.fn()
  306. const mountSpy = vi.fn()
  307. const activeSpy = vi.fn()
  308. const deactivatedSpy = vi.fn()
  309. const Child = defineVaporComponent({
  310. name: 'original',
  311. __hmrId: childId,
  312. setup() {
  313. onUnmounted(unmountSpy)
  314. const count = ref(0)
  315. return { count }
  316. },
  317. render: compileToFunction(`<div>{{ count }}</div>`),
  318. })
  319. createRecord(childId, Child as any)
  320. const Parent = defineVaporComponent({
  321. components: { Child },
  322. setup() {
  323. function onLeave(_: any, done: Function) {
  324. setTimeout(done, 0)
  325. }
  326. const toggle = ref(true)
  327. return { toggle, onLeave }
  328. },
  329. render: compileToFunction(
  330. `<button @click="toggle = !toggle" />
  331. <Transition mode="out-in" @leave="onLeave">
  332. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  333. </Transition>`,
  334. ),
  335. })
  336. define(Parent).create().mount(root)
  337. expect(root.innerHTML).toBe(`<button></button><div>0</div><!--if-->`)
  338. reload(childId, {
  339. name: 'updated',
  340. __hmrId: childId,
  341. __vapor: true,
  342. setup() {
  343. onMounted(mountSpy)
  344. onUnmounted(unmountSpy)
  345. onActivated(activeSpy)
  346. onDeactivated(deactivatedSpy)
  347. const count = ref(1)
  348. return { count }
  349. },
  350. render: compileToFunction(`<div>{{ count }}</div>`),
  351. })
  352. await nextTick()
  353. await new Promise(r => setTimeout(r, 0))
  354. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  355. expect(unmountSpy).toHaveBeenCalledTimes(1)
  356. expect(mountSpy).toHaveBeenCalledTimes(1)
  357. expect(activeSpy).toHaveBeenCalledTimes(1)
  358. expect(deactivatedSpy).toHaveBeenCalledTimes(0)
  359. // should not unmount when toggling
  360. triggerEvent('click', root.children[0] as Element)
  361. await nextTick()
  362. await new Promise(r => setTimeout(r, 0))
  363. expect(root.innerHTML).toBe(`<button></button><!--if-->`)
  364. expect(unmountSpy).toHaveBeenCalledTimes(1)
  365. expect(mountSpy).toHaveBeenCalledTimes(1)
  366. expect(activeSpy).toHaveBeenCalledTimes(1)
  367. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  368. // should not mount when toggling
  369. triggerEvent('click', root.children[0] as Element)
  370. await nextTick()
  371. expect(root.innerHTML).toBe(`<button></button><div>1</div><!--if-->`)
  372. expect(unmountSpy).toHaveBeenCalledTimes(1)
  373. expect(mountSpy).toHaveBeenCalledTimes(1)
  374. expect(activeSpy).toHaveBeenCalledTimes(2)
  375. expect(deactivatedSpy).toHaveBeenCalledTimes(1)
  376. })
  377. // TODO: renderEffect not re-run after child reload
  378. // it requires parent rerender to align with vdom
  379. test.todo('reload: avoid infinite recursion', async () => {
  380. const root = document.createElement('div')
  381. document.body.appendChild(root)
  382. const childId = 'test-child-6930'
  383. const unmountSpy = vi.fn()
  384. const mountSpy = vi.fn()
  385. const Child = defineVaporComponent({
  386. __hmrId: childId,
  387. setup(_, { expose }) {
  388. const count = ref(0)
  389. expose({
  390. count,
  391. })
  392. onUnmounted(unmountSpy)
  393. return { count }
  394. },
  395. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  396. })
  397. createRecord(childId, Child as any)
  398. const Parent = defineVaporComponent({
  399. setup() {
  400. const com1 = ref()
  401. const changeRef1 = (value: any) => (com1.value = value)
  402. const com2 = ref()
  403. const changeRef2 = (value: any) => (com2.value = value)
  404. const setRef = createTemplateRefSetter()
  405. const n0 = createComponent(Child)
  406. setRef(n0, changeRef1)
  407. const n1 = createComponent(Child)
  408. setRef(n1, changeRef2)
  409. const n2 = template(' ')() as any
  410. renderEffect(() => {
  411. setText(n2, toDisplayString(com1.value.count))
  412. })
  413. return [n0, n1, n2]
  414. },
  415. })
  416. define(Parent).create().mount(root)
  417. await nextTick()
  418. expect(root.innerHTML).toBe(`<div>0</div><div>0</div>0`)
  419. reload(childId, {
  420. __hmrId: childId,
  421. __vapor: true,
  422. setup() {
  423. onMounted(mountSpy)
  424. const count = ref(1)
  425. return { count }
  426. },
  427. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  428. })
  429. await nextTick()
  430. expect(root.innerHTML).toBe(`<div>1</div><div>1</div>1`)
  431. expect(unmountSpy).toHaveBeenCalledTimes(2)
  432. expect(mountSpy).toHaveBeenCalledTimes(2)
  433. })
  434. test('static el reference', async () => {
  435. const root = document.createElement('div')
  436. document.body.appendChild(root)
  437. const id = 'test-static-el'
  438. const template = `<div>
  439. <div>{{ count }}</div>
  440. <button @click="count++">++</button>
  441. </div>`
  442. const Comp = defineVaporComponent({
  443. __hmrId: id,
  444. setup() {
  445. const count = ref(0)
  446. return { count }
  447. },
  448. render: compileToFunction(template),
  449. })
  450. createRecord(id, Comp as any)
  451. define(Comp).create().mount(root)
  452. expect(root.innerHTML).toBe(`<div><div>0</div><button>++</button></div>`)
  453. // 1. click to trigger update
  454. triggerEvent('click', root.children[0].children[1] as Element)
  455. await nextTick()
  456. expect(root.innerHTML).toBe(`<div><div>1</div><button>++</button></div>`)
  457. // 2. trigger HMR
  458. rerender(
  459. id,
  460. compileToFunction(template.replace(`<button`, `<button class="foo"`)),
  461. )
  462. expect(root.innerHTML).toBe(
  463. `<div><div>1</div><button class="foo">++</button></div>`,
  464. )
  465. })
  466. test('force update child component w/ static props', () => {
  467. const root = document.createElement('div')
  468. const parentId = 'test-force-props-parent'
  469. const childId = 'test-force-props-child'
  470. const Child = defineVaporComponent({
  471. __hmrId: childId,
  472. props: {
  473. msg: String,
  474. },
  475. render: compileToFunction(`<div>{{ msg }}</div>`, {
  476. bindingMetadata: {
  477. msg: BindingTypes.PROPS,
  478. },
  479. }),
  480. })
  481. createRecord(childId, Child as any)
  482. const Parent = defineVaporComponent({
  483. __hmrId: parentId,
  484. components: { Child },
  485. render: compileToFunction(`<Child msg="foo" />`),
  486. })
  487. createRecord(parentId, Parent as any)
  488. define(Parent).create().mount(root)
  489. expect(root.innerHTML).toBe(`<div>foo</div>`)
  490. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  491. expect(root.innerHTML).toBe(`<div>bar</div>`)
  492. })
  493. test('remove static class from parent', () => {
  494. const root = document.createElement('div')
  495. const parentId = 'test-force-class-parent'
  496. const childId = 'test-force-class-child'
  497. const Child = defineVaporComponent({
  498. __hmrId: childId,
  499. render: compileToFunction(`<div>child</div>`),
  500. })
  501. createRecord(childId, Child as any)
  502. const Parent = defineVaporComponent({
  503. __hmrId: parentId,
  504. components: { Child },
  505. render: compileToFunction(`<Child class="test" />`),
  506. })
  507. createRecord(parentId, Parent as any)
  508. define(Parent).create().mount(root)
  509. expect(root.innerHTML).toBe(`<div class="test">child</div>`)
  510. rerender(parentId, compileToFunction(`<Child/>`))
  511. expect(root.innerHTML).toBe(`<div>child</div>`)
  512. })
  513. test('rerender if any parent in the parent chain', () => {
  514. const root = document.createElement('div')
  515. const parent = 'test-force-props-parent-'
  516. const childId = 'test-force-props-child'
  517. const numberOfParents = 5
  518. const Child = defineVaporComponent({
  519. __hmrId: childId,
  520. render: compileToFunction(`<div>child</div>`),
  521. })
  522. createRecord(childId, Child as any)
  523. const components: VaporComponent[] = []
  524. for (let i = 0; i < numberOfParents; i++) {
  525. const parentId = `${parent}${i}`
  526. const parentComp: VaporComponent = {
  527. __vapor: true,
  528. __hmrId: parentId,
  529. }
  530. components.push(parentComp)
  531. if (i === 0) {
  532. parentComp.render = compileToFunction(`<Child />`)
  533. parentComp.components = {
  534. Child,
  535. }
  536. } else {
  537. parentComp.render = compileToFunction(`<Parent />`)
  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. components: { Child },
  903. setup() {
  904. const msg = ref('root')
  905. return { msg }
  906. },
  907. render: compileToFunction(`<Child/><div>{{ msg }}</div>`),
  908. }).create()
  909. createRecord(parentId, Parent as any)
  910. mount(root)
  911. expect(root.innerHTML).toMatchInlineSnapshot(
  912. `"<div>child</div><div>root</div>"`,
  913. )
  914. // reload child
  915. reload(childId, {
  916. __hmrId: childId,
  917. __vapor: true,
  918. setup() {
  919. const msg = ref('child changed')
  920. return { msg }
  921. },
  922. render: compileToFunction(`<div>{{ msg }}</div>`),
  923. })
  924. expect(root.innerHTML).toMatchInlineSnapshot(
  925. `"<div>child changed</div><div>root</div>"`,
  926. )
  927. // reload child again
  928. reload(childId, {
  929. __hmrId: childId,
  930. __vapor: true,
  931. setup() {
  932. const msg = ref('child changed2')
  933. return { msg }
  934. },
  935. render: compileToFunction(`<div>{{ msg }}</div>`),
  936. })
  937. expect(root.innerHTML).toMatchInlineSnapshot(
  938. `"<div>child changed2</div><div>root</div>"`,
  939. )
  940. // reload parent
  941. reload(parentId, {
  942. __hmrId: parentId,
  943. __vapor: true,
  944. // @ts-expect-error
  945. components: { Child },
  946. setup() {
  947. const msg = ref('root changed')
  948. return { msg }
  949. },
  950. render: compileToFunction(`<Child/><div>{{ msg }}</div>`),
  951. })
  952. expect(root.innerHTML).toMatchInlineSnapshot(
  953. `"<div>child changed2</div><div>root changed</div>"`,
  954. )
  955. })
  956. // Vapor router-view has no render function (setup-only).
  957. // When HMR rerender is triggered, the setup function is re-executed.
  958. // Ensure provide() warning is suppressed.
  959. test('rerender setup-only component', async () => {
  960. const childId = 'test-child-reload-01'
  961. const Child = defineVaporComponent({
  962. __hmrId: childId,
  963. render: compileToFunction(`<div>foo</div>`),
  964. })
  965. createRecord(childId, Child as any)
  966. // without a render function
  967. const Parent = defineVaporComponent({
  968. setup() {
  969. provide('foo', 'bar')
  970. return createComponent(Child)
  971. },
  972. })
  973. const { html } = define({
  974. setup() {
  975. return createComponent(Parent)
  976. },
  977. }).render()
  978. expect(html()).toBe('<div>foo</div>')
  979. // will trigger parent rerender
  980. reload(childId, {
  981. __hmrId: childId,
  982. render: compileToFunction(`<div>bar</div>`),
  983. })
  984. await nextTick()
  985. expect(html()).toBe('<div>bar</div>')
  986. expect('provide() can only be used inside setup()').not.toHaveBeenWarned()
  987. })
  988. describe('switch vapor/vdom modes', () => {
  989. test('vapor -> vdom', async () => {
  990. const id = 'vapor-to-vdom'
  991. const Comp = {
  992. __vapor: true,
  993. __hmrId: id,
  994. render() {
  995. return template('<div>foo</div>')()
  996. },
  997. }
  998. createRecord(id, Comp)
  999. const App = {
  1000. render() {
  1001. return h(Comp as any)
  1002. },
  1003. }
  1004. const root = document.createElement('div')
  1005. const app = createApp(App)
  1006. app.use(vaporInteropPlugin)
  1007. app.mount(root)
  1008. expect(root.innerHTML).toBe('<div>foo</div>')
  1009. // switch to vdom
  1010. reload(id, {
  1011. __hmrId: id,
  1012. render() {
  1013. return h('div', 'bar')
  1014. },
  1015. })
  1016. await nextTick()
  1017. expect(root.innerHTML).toBe('<div>bar</div>')
  1018. })
  1019. test('vdom -> vapor', async () => {
  1020. const id = 'vdom-to-vapor'
  1021. const Comp = {
  1022. __hmrId: id,
  1023. render() {
  1024. return h('div', 'foo')
  1025. },
  1026. }
  1027. createRecord(id, Comp)
  1028. const App = {
  1029. render() {
  1030. return h(Comp)
  1031. },
  1032. }
  1033. const root = document.createElement('div')
  1034. const app = createApp(App)
  1035. app.use(vaporInteropPlugin)
  1036. app.mount(root)
  1037. expect(root.innerHTML).toBe('<div>foo</div>')
  1038. // switch to vapor
  1039. reload(id, {
  1040. __vapor: true,
  1041. __hmrId: id,
  1042. render() {
  1043. return template('<div>bar</div>')()
  1044. },
  1045. })
  1046. await nextTick()
  1047. expect(root.innerHTML).toBe('<div>bar</div>')
  1048. })
  1049. })
  1050. })