hmr.spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  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. describe('hot module replacement', () => {
  29. test('inject global runtime', () => {
  30. expect(createRecord).toBeDefined()
  31. expect(rerender).toBeDefined()
  32. expect(reload).toBeDefined()
  33. })
  34. test('createRecord', () => {
  35. expect(createRecord('test1', {})).toBe(true)
  36. // if id has already been created, should return false
  37. expect(createRecord('test1', {})).toBe(false)
  38. })
  39. test('rerender', async () => {
  40. const root = nodeOps.createElement('div')
  41. const parentId = 'test2-parent'
  42. const childId = 'test2-child'
  43. const Child: ComponentOptions = {
  44. __hmrId: childId,
  45. render: compileToFunction(`<div><slot/></div>`),
  46. }
  47. createRecord(childId, Child)
  48. const Parent: ComponentOptions = {
  49. __hmrId: parentId,
  50. data() {
  51. return { count: 0 }
  52. },
  53. components: { Child },
  54. render: compileToFunction(
  55. `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`,
  56. ),
  57. }
  58. createRecord(parentId, Parent)
  59. render(h(Parent), root)
  60. expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
  61. // Perform some state change. This change should be preserved after the
  62. // re-render!
  63. triggerEvent(root.children[0] as TestElement, 'click')
  64. await nextTick()
  65. expect(serializeInner(root)).toBe(`<div>1<div>1</div></div>`)
  66. // // Update text while preserving state
  67. rerender(
  68. parentId,
  69. compileToFunction(
  70. `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`,
  71. ),
  72. )
  73. expect(serializeInner(root)).toBe(`<div>1!<div>1</div></div>`)
  74. // Should force child update on slot content change
  75. rerender(
  76. parentId,
  77. compileToFunction(
  78. `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`,
  79. ),
  80. )
  81. expect(serializeInner(root)).toBe(`<div>1!<div>1!</div></div>`)
  82. // Should force update element children despite block optimization
  83. rerender(
  84. parentId,
  85. compileToFunction(
  86. `<div @click="count++">{{ count }}<span>{{ count }}</span>
  87. <Child>{{ count }}!</Child>
  88. </div>`,
  89. ),
  90. )
  91. expect(serializeInner(root)).toBe(`<div>1<span>1</span><div>1!</div></div>`)
  92. // Should force update child slot elements
  93. rerender(
  94. parentId,
  95. compileToFunction(
  96. `<div @click="count++">
  97. <Child><span>{{ count }}</span></Child>
  98. </div>`,
  99. ),
  100. )
  101. expect(serializeInner(root)).toBe(`<div><div><span>1</span></div></div>`)
  102. })
  103. test('reload', async () => {
  104. const root = nodeOps.createElement('div')
  105. const childId = 'test3-child'
  106. const unmountSpy = vi.fn()
  107. const mountSpy = vi.fn()
  108. const Child: ComponentOptions = {
  109. __hmrId: childId,
  110. data() {
  111. return { count: 0 }
  112. },
  113. unmounted: unmountSpy,
  114. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  115. }
  116. createRecord(childId, Child)
  117. const Parent: ComponentOptions = {
  118. render: () => h(Child),
  119. }
  120. render(h(Parent), root)
  121. expect(serializeInner(root)).toBe(`<div>0</div>`)
  122. reload(childId, {
  123. __hmrId: childId,
  124. data() {
  125. return { count: 1 }
  126. },
  127. mounted: mountSpy,
  128. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  129. })
  130. await nextTick()
  131. expect(serializeInner(root)).toBe(`<div>1</div>`)
  132. expect(unmountSpy).toHaveBeenCalledTimes(1)
  133. expect(mountSpy).toHaveBeenCalledTimes(1)
  134. })
  135. // #7042
  136. test('reload KeepAlive slot', async () => {
  137. const root = nodeOps.createElement('div')
  138. const childId = 'test-child-keep-alive'
  139. const unmountSpy = vi.fn()
  140. const mountSpy = vi.fn()
  141. const activeSpy = vi.fn()
  142. const deactiveSpy = vi.fn()
  143. const Child: ComponentOptions = {
  144. __hmrId: childId,
  145. data() {
  146. return { count: 0 }
  147. },
  148. unmounted: unmountSpy,
  149. render: compileToFunction(`<div>{{ count }}</div>`),
  150. }
  151. createRecord(childId, Child)
  152. const Parent: ComponentOptions = {
  153. components: { Child },
  154. data() {
  155. return { toggle: true }
  156. },
  157. render: compileToFunction(
  158. `<button @click="toggle = !toggle" />
  159. <KeepAlive><Child v-if="toggle" /></KeepAlive>`,
  160. ),
  161. }
  162. render(h(Parent), root)
  163. expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
  164. reload(childId, {
  165. __hmrId: childId,
  166. data() {
  167. return { count: 1 }
  168. },
  169. mounted: mountSpy,
  170. unmounted: unmountSpy,
  171. activated: activeSpy,
  172. deactivated: deactiveSpy,
  173. render: compileToFunction(`<div>{{ count }}</div>`),
  174. })
  175. await nextTick()
  176. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  177. expect(unmountSpy).toHaveBeenCalledTimes(1)
  178. expect(mountSpy).toHaveBeenCalledTimes(1)
  179. expect(activeSpy).toHaveBeenCalledTimes(1)
  180. expect(deactiveSpy).toHaveBeenCalledTimes(0)
  181. // should not unmount when toggling
  182. triggerEvent(root.children[1] as TestElement, 'click')
  183. await nextTick()
  184. expect(unmountSpy).toHaveBeenCalledTimes(1)
  185. expect(mountSpy).toHaveBeenCalledTimes(1)
  186. expect(activeSpy).toHaveBeenCalledTimes(1)
  187. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  188. // should not mount when toggling
  189. triggerEvent(root.children[1] as TestElement, 'click')
  190. await nextTick()
  191. expect(unmountSpy).toHaveBeenCalledTimes(1)
  192. expect(mountSpy).toHaveBeenCalledTimes(1)
  193. expect(activeSpy).toHaveBeenCalledTimes(2)
  194. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  195. })
  196. // #7121
  197. test('reload KeepAlive slot in Transition', async () => {
  198. const root = nodeOps.createElement('div')
  199. const childId = 'test-transition-keep-alive-reload'
  200. const unmountSpy = vi.fn()
  201. const mountSpy = vi.fn()
  202. const activeSpy = vi.fn()
  203. const deactiveSpy = vi.fn()
  204. const Child: ComponentOptions = {
  205. __hmrId: childId,
  206. data() {
  207. return { count: 0 }
  208. },
  209. unmounted: unmountSpy,
  210. render: compileToFunction(`<div>{{ count }}</div>`),
  211. }
  212. createRecord(childId, Child)
  213. const Parent: ComponentOptions = {
  214. components: { Child },
  215. data() {
  216. return { toggle: true }
  217. },
  218. render: compileToFunction(
  219. `<button @click="toggle = !toggle" />
  220. <BaseTransition>
  221. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  222. </BaseTransition>`,
  223. ),
  224. }
  225. render(h(Parent), root)
  226. expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
  227. reload(childId, {
  228. __hmrId: childId,
  229. data() {
  230. return { count: 1 }
  231. },
  232. mounted: mountSpy,
  233. unmounted: unmountSpy,
  234. activated: activeSpy,
  235. deactivated: deactiveSpy,
  236. render: compileToFunction(`<div>{{ count }}</div>`),
  237. })
  238. await nextTick()
  239. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  240. expect(unmountSpy).toHaveBeenCalledTimes(1)
  241. expect(mountSpy).toHaveBeenCalledTimes(1)
  242. expect(activeSpy).toHaveBeenCalledTimes(1)
  243. expect(deactiveSpy).toHaveBeenCalledTimes(0)
  244. // should not unmount when toggling
  245. triggerEvent(root.children[1] as TestElement, 'click')
  246. await nextTick()
  247. expect(serializeInner(root)).toBe(`<button></button><!--v-if-->`)
  248. expect(unmountSpy).toHaveBeenCalledTimes(1)
  249. expect(mountSpy).toHaveBeenCalledTimes(1)
  250. expect(activeSpy).toHaveBeenCalledTimes(1)
  251. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  252. // should not mount when toggling
  253. triggerEvent(root.children[1] as TestElement, 'click')
  254. await nextTick()
  255. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  256. expect(unmountSpy).toHaveBeenCalledTimes(1)
  257. expect(mountSpy).toHaveBeenCalledTimes(1)
  258. expect(activeSpy).toHaveBeenCalledTimes(2)
  259. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  260. })
  261. test('reload KeepAlive slot in Transition with out-in', async () => {
  262. const root = nodeOps.createElement('div')
  263. const childId = 'test-transition-keep-alive-reload-with-out-in'
  264. const unmountSpy = vi.fn()
  265. const mountSpy = vi.fn()
  266. const activeSpy = vi.fn()
  267. const deactiveSpy = vi.fn()
  268. const Child: ComponentOptions = {
  269. __hmrId: childId,
  270. name: 'original',
  271. data() {
  272. return { count: 0 }
  273. },
  274. unmounted: unmountSpy,
  275. render: compileToFunction(`<div>{{ count }}</div>`),
  276. }
  277. createRecord(childId, Child)
  278. const Parent: ComponentOptions = {
  279. components: { Child },
  280. data() {
  281. return { toggle: true }
  282. },
  283. methods: {
  284. // @ts-expect-error
  285. onLeave(_, done) {
  286. setTimeout(done, 0)
  287. },
  288. },
  289. render: compileToFunction(
  290. `<button @click="toggle = !toggle" />
  291. <BaseTransition mode="out-in" @leave="onLeave">
  292. <KeepAlive><Child v-if="toggle" /></KeepAlive>
  293. </BaseTransition>`,
  294. ),
  295. }
  296. render(h(Parent), root)
  297. expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
  298. reload(childId, {
  299. __hmrId: childId,
  300. name: 'updated',
  301. data() {
  302. return { count: 1 }
  303. },
  304. mounted: mountSpy,
  305. unmounted: unmountSpy,
  306. activated: activeSpy,
  307. deactivated: deactiveSpy,
  308. render: compileToFunction(`<div>{{ count }}</div>`),
  309. })
  310. await nextTick()
  311. await new Promise(r => setTimeout(r, 0))
  312. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  313. expect(unmountSpy).toHaveBeenCalledTimes(1)
  314. expect(mountSpy).toHaveBeenCalledTimes(1)
  315. expect(activeSpy).toHaveBeenCalledTimes(1)
  316. expect(deactiveSpy).toHaveBeenCalledTimes(0)
  317. // should not unmount when toggling
  318. triggerEvent(root.children[1] as TestElement, 'click')
  319. await nextTick()
  320. await new Promise(r => setTimeout(r, 0))
  321. expect(serializeInner(root)).toBe(`<button></button><!--v-if-->`)
  322. expect(unmountSpy).toHaveBeenCalledTimes(1)
  323. expect(mountSpy).toHaveBeenCalledTimes(1)
  324. expect(activeSpy).toHaveBeenCalledTimes(1)
  325. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  326. // should not mount when toggling
  327. triggerEvent(root.children[1] as TestElement, 'click')
  328. await nextTick()
  329. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  330. expect(unmountSpy).toHaveBeenCalledTimes(1)
  331. expect(mountSpy).toHaveBeenCalledTimes(1)
  332. expect(activeSpy).toHaveBeenCalledTimes(2)
  333. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  334. })
  335. test('reload class component', async () => {
  336. const root = nodeOps.createElement('div')
  337. const childId = 'test4-child'
  338. const unmountSpy = vi.fn()
  339. const mountSpy = vi.fn()
  340. class Child {
  341. static __vccOpts: ComponentOptions = {
  342. __hmrId: childId,
  343. data() {
  344. return { count: 0 }
  345. },
  346. unmounted: unmountSpy,
  347. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  348. }
  349. }
  350. createRecord(childId, Child)
  351. const Parent: ComponentOptions = {
  352. render: () => h(Child),
  353. }
  354. render(h(Parent), root)
  355. expect(serializeInner(root)).toBe(`<div>0</div>`)
  356. class UpdatedChild {
  357. static __vccOpts: ComponentOptions = {
  358. __hmrId: childId,
  359. data() {
  360. return { count: 1 }
  361. },
  362. mounted: mountSpy,
  363. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  364. }
  365. }
  366. reload(childId, UpdatedChild)
  367. await nextTick()
  368. expect(serializeInner(root)).toBe(`<div>1</div>`)
  369. expect(unmountSpy).toHaveBeenCalledTimes(1)
  370. expect(mountSpy).toHaveBeenCalledTimes(1)
  371. })
  372. // #6930
  373. test('reload: avoid infinite recursion', async () => {
  374. const root = nodeOps.createElement('div')
  375. const childId = 'test-child-6930'
  376. const unmountSpy = vi.fn()
  377. const mountSpy = vi.fn()
  378. const Child: ComponentOptions = {
  379. __hmrId: childId,
  380. data() {
  381. return { count: 0 }
  382. },
  383. expose: ['count'],
  384. unmounted: unmountSpy,
  385. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  386. }
  387. createRecord(childId, Child)
  388. const Parent: ComponentOptions = {
  389. setup() {
  390. const com = ref()
  391. const changeRef = (value: any) => {
  392. com.value = value
  393. }
  394. return () => [h(Child, { ref: changeRef }), com.value?.count]
  395. },
  396. }
  397. render(h(Parent), root)
  398. await nextTick()
  399. expect(serializeInner(root)).toBe(`<div>0</div>0`)
  400. reload(childId, {
  401. __hmrId: childId,
  402. data() {
  403. return { count: 1 }
  404. },
  405. mounted: mountSpy,
  406. render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
  407. })
  408. await nextTick()
  409. expect(serializeInner(root)).toBe(`<div>1</div>1`)
  410. expect(unmountSpy).toHaveBeenCalledTimes(1)
  411. expect(mountSpy).toHaveBeenCalledTimes(1)
  412. })
  413. // #1156 - static nodes should retain DOM element reference across updates
  414. // when HMR is active
  415. test('static el reference', async () => {
  416. const root = nodeOps.createElement('div')
  417. const id = 'test-static-el'
  418. const template = `<div>
  419. <div>{{ count }}</div>
  420. <button @click="count++">++</button>
  421. </div>`
  422. const Comp: ComponentOptions = {
  423. __hmrId: id,
  424. data() {
  425. return { count: 0 }
  426. },
  427. render: compileToFunction(template),
  428. }
  429. createRecord(id, Comp)
  430. render(h(Comp), root)
  431. expect(serializeInner(root)).toBe(
  432. `<div><div>0</div><button>++</button></div>`,
  433. )
  434. // 1. click to trigger update
  435. triggerEvent((root as any).children[0].children[1], 'click')
  436. await nextTick()
  437. expect(serializeInner(root)).toBe(
  438. `<div><div>1</div><button>++</button></div>`,
  439. )
  440. // 2. trigger HMR
  441. rerender(
  442. id,
  443. compileToFunction(template.replace(`<button`, `<button class="foo"`)),
  444. )
  445. expect(serializeInner(root)).toBe(
  446. `<div><div>1</div><button class="foo">++</button></div>`,
  447. )
  448. })
  449. // #1157 - component should force full props update when HMR is active
  450. test('force update child component w/ static props', () => {
  451. const root = nodeOps.createElement('div')
  452. const parentId = 'test-force-props-parent'
  453. const childId = 'test-force-props-child'
  454. const Child: ComponentOptions = {
  455. __hmrId: childId,
  456. props: {
  457. msg: String,
  458. },
  459. render: compileToFunction(`<div>{{ msg }}</div>`),
  460. }
  461. createRecord(childId, Child)
  462. const Parent: ComponentOptions = {
  463. __hmrId: parentId,
  464. components: { Child },
  465. render: compileToFunction(`<Child msg="foo" />`),
  466. }
  467. createRecord(parentId, Parent)
  468. render(h(Parent), root)
  469. expect(serializeInner(root)).toBe(`<div>foo</div>`)
  470. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  471. expect(serializeInner(root)).toBe(`<div>bar</div>`)
  472. })
  473. // #1305 - component should remove class
  474. test('remove static class from parent', () => {
  475. const root = nodeOps.createElement('div')
  476. const parentId = 'test-force-class-parent'
  477. const childId = 'test-force-class-child'
  478. const Child: ComponentOptions = {
  479. __hmrId: childId,
  480. render: compileToFunction(`<div>child</div>`),
  481. }
  482. createRecord(childId, Child)
  483. const Parent: ComponentOptions = {
  484. __hmrId: parentId,
  485. components: { Child },
  486. render: compileToFunction(`<Child class="test" />`),
  487. }
  488. createRecord(parentId, Parent)
  489. render(h(Parent), root)
  490. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  491. rerender(parentId, compileToFunction(`<Child/>`))
  492. expect(serializeInner(root)).toBe(`<div>child</div>`)
  493. })
  494. test('rerender if any parent in the parent chain', () => {
  495. const root = nodeOps.createElement('div')
  496. const parent = 'test-force-props-parent-'
  497. const childId = 'test-force-props-child'
  498. const numberOfParents = 5
  499. const Child: ComponentOptions = {
  500. __hmrId: childId,
  501. render: compileToFunction(`<div>child</div>`),
  502. }
  503. createRecord(childId, Child)
  504. const components: ComponentOptions[] = []
  505. for (let i = 0; i < numberOfParents; i++) {
  506. const parentId = `${parent}${i}`
  507. const parentComp: ComponentOptions = {
  508. __hmrId: parentId,
  509. }
  510. components.push(parentComp)
  511. if (i === 0) {
  512. parentComp.render = compileToFunction(`<Child />`)
  513. parentComp.components = {
  514. Child,
  515. }
  516. } else {
  517. parentComp.render = compileToFunction(`<Parent />`)
  518. parentComp.components = {
  519. Parent: components[i - 1],
  520. }
  521. }
  522. createRecord(parentId, parentComp)
  523. }
  524. const last = components[components.length - 1]
  525. render(h(last), root)
  526. expect(serializeInner(root)).toBe(`<div>child</div>`)
  527. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  528. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  529. })
  530. // #3302
  531. test('rerender with Teleport', () => {
  532. const root = nodeOps.createElement('div')
  533. const target = nodeOps.createElement('div')
  534. const parentId = 'parent-teleport'
  535. const Child: ComponentOptions = {
  536. data() {
  537. return {
  538. // style is used to ensure that the div tag will be tracked by Teleport
  539. style: {},
  540. target,
  541. }
  542. },
  543. render: compileToFunction(`
  544. <teleport :to="target">
  545. <div :style="style">
  546. <slot/>
  547. </div>
  548. </teleport>
  549. `),
  550. }
  551. const Parent: ComponentOptions = {
  552. __hmrId: parentId,
  553. components: { Child },
  554. render: compileToFunction(`
  555. <Child>
  556. <template #default>
  557. <div>1</div>
  558. </template>
  559. </Child>
  560. `),
  561. }
  562. createRecord(parentId, Parent)
  563. render(h(Parent), root)
  564. expect(serializeInner(root)).toBe(
  565. `<!--teleport start--><!--teleport end-->`,
  566. )
  567. expect(serializeInner(target)).toBe(`<div style={}><div>1</div></div>`)
  568. rerender(
  569. parentId,
  570. compileToFunction(`
  571. <Child>
  572. <template #default>
  573. <div>1</div>
  574. <div>2</div>
  575. </template>
  576. </Child>
  577. `),
  578. )
  579. expect(serializeInner(root)).toBe(
  580. `<!--teleport start--><!--teleport end-->`,
  581. )
  582. expect(serializeInner(target)).toBe(
  583. `<div style={}><div>1</div><div>2</div></div>`,
  584. )
  585. })
  586. // #4174
  587. test('with global mixins', async () => {
  588. const childId = 'hmr-global-mixin'
  589. const createSpy1 = vi.fn()
  590. const createSpy2 = vi.fn()
  591. const Child: ComponentOptions = {
  592. __hmrId: childId,
  593. created: createSpy1,
  594. render() {
  595. return h('div')
  596. },
  597. }
  598. createRecord(childId, Child)
  599. const Parent: ComponentOptions = {
  600. render: () => h(Child),
  601. }
  602. const app = createApp(Parent)
  603. app.mixin({})
  604. const root = nodeOps.createElement('div')
  605. app.mount(root)
  606. expect(createSpy1).toHaveBeenCalledTimes(1)
  607. expect(createSpy2).toHaveBeenCalledTimes(0)
  608. reload(childId, {
  609. __hmrId: childId,
  610. created: createSpy2,
  611. render() {
  612. return h('div')
  613. },
  614. })
  615. await nextTick()
  616. expect(createSpy1).toHaveBeenCalledTimes(1)
  617. expect(createSpy2).toHaveBeenCalledTimes(1)
  618. })
  619. // #4757
  620. test('rerender for component that has no active instance yet', () => {
  621. const id = 'no-active-instance-rerender'
  622. const Foo: ComponentOptions = {
  623. __hmrId: id,
  624. render: () => 'foo',
  625. }
  626. createRecord(id, Foo)
  627. rerender(id, () => 'bar')
  628. const root = nodeOps.createElement('div')
  629. render(h(Foo), root)
  630. expect(serializeInner(root)).toBe('bar')
  631. })
  632. test('reload for component that has no active instance yet', () => {
  633. const id = 'no-active-instance-reload'
  634. const Foo: ComponentOptions = {
  635. __hmrId: id,
  636. render: () => 'foo',
  637. }
  638. createRecord(id, Foo)
  639. reload(id, {
  640. __hmrId: id,
  641. render: () => 'bar',
  642. })
  643. const root = nodeOps.createElement('div')
  644. render(h(Foo), root)
  645. expect(serializeInner(root)).toBe('bar')
  646. })
  647. // #7155 - force HMR on slots content update
  648. test('force update slot content change', () => {
  649. const root = nodeOps.createElement('div')
  650. const parentId = 'test-force-computed-parent'
  651. const childId = 'test-force-computed-child'
  652. const Child: ComponentOptions = {
  653. __hmrId: childId,
  654. computed: {
  655. slotContent() {
  656. return this.$slots.default?.()
  657. },
  658. },
  659. render: compileToFunction(`<component :is="() => slotContent" />`),
  660. }
  661. createRecord(childId, Child)
  662. const Parent: ComponentOptions = {
  663. __hmrId: parentId,
  664. components: { Child },
  665. render: compileToFunction(`<Child>1</Child>`),
  666. }
  667. createRecord(parentId, Parent)
  668. render(h(Parent), root)
  669. expect(serializeInner(root)).toBe(`1`)
  670. rerender(parentId, compileToFunction(`<Child>2</Child>`))
  671. expect(serializeInner(root)).toBe(`2`)
  672. })
  673. // #6978, #7138, #7114
  674. test('hoisted children array inside v-for', () => {
  675. const root = nodeOps.createElement('div')
  676. const appId = 'test-app-id'
  677. const App: ComponentOptions = {
  678. __hmrId: appId,
  679. render: compileToFunction(
  680. `<div v-for="item of 2">
  681. <div>1</div>
  682. </div>
  683. <p>2</p>
  684. <p>3</p>`,
  685. ),
  686. }
  687. createRecord(appId, App)
  688. render(h(App), root)
  689. expect(serializeInner(root)).toBe(
  690. `<div><div>1</div></div><div><div>1</div></div><p>2</p><p>3</p>`,
  691. )
  692. // move the <p>3</p> into the <div>1</div>
  693. rerender(
  694. appId,
  695. compileToFunction(
  696. `<div v-for="item of 2">
  697. <div>1<p>3</p></div>
  698. </div>
  699. <p>2</p>`,
  700. ),
  701. )
  702. expect(serializeInner(root)).toBe(
  703. `<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`,
  704. )
  705. })
  706. })