hmr.spec.ts 29 KB

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