hmr.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. import { vi } from 'vitest'
  2. import { HMRRuntime } from '../src/hmr'
  3. import '../src/hmr'
  4. import { ComponentOptions, InternalRenderFunction } from '../src/component'
  5. import {
  6. render,
  7. nodeOps,
  8. h,
  9. serializeInner,
  10. triggerEvent,
  11. TestElement,
  12. nextTick
  13. } from '@vue/runtime-test'
  14. import * as runtimeTest from '@vue/runtime-test'
  15. import { registerRuntimeCompiler, createApp } 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)
  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"></button><KeepAlive><Child v-if="toggle" /></KeepAlive>`
  159. )
  160. }
  161. render(h(Parent), root)
  162. expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
  163. reload(childId, {
  164. __hmrId: childId,
  165. data() {
  166. return { count: 1 }
  167. },
  168. mounted: mountSpy,
  169. unmounted: unmountSpy,
  170. activated: activeSpy,
  171. deactivated: deactiveSpy,
  172. render: compileToFunction(`<div>{{ count }}</div>`)
  173. })
  174. await nextTick()
  175. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  176. expect(unmountSpy).toHaveBeenCalledTimes(1)
  177. expect(mountSpy).toHaveBeenCalledTimes(1)
  178. expect(activeSpy).toHaveBeenCalledTimes(1)
  179. expect(deactiveSpy).toHaveBeenCalledTimes(0)
  180. // should not unmount when toggling
  181. triggerEvent(root.children[1] as TestElement, 'click')
  182. await nextTick()
  183. expect(unmountSpy).toHaveBeenCalledTimes(1)
  184. expect(mountSpy).toHaveBeenCalledTimes(1)
  185. expect(activeSpy).toHaveBeenCalledTimes(1)
  186. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  187. // should not mount when toggling
  188. triggerEvent(root.children[1] as TestElement, 'click')
  189. await nextTick()
  190. expect(unmountSpy).toHaveBeenCalledTimes(1)
  191. expect(mountSpy).toHaveBeenCalledTimes(1)
  192. expect(activeSpy).toHaveBeenCalledTimes(2)
  193. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  194. })
  195. test('reload class component', async () => {
  196. const root = nodeOps.createElement('div')
  197. const childId = 'test4-child'
  198. const unmountSpy = vi.fn()
  199. const mountSpy = vi.fn()
  200. class Child {
  201. static __vccOpts: ComponentOptions = {
  202. __hmrId: childId,
  203. data() {
  204. return { count: 0 }
  205. },
  206. unmounted: unmountSpy,
  207. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  208. }
  209. }
  210. createRecord(childId, Child)
  211. const Parent: ComponentOptions = {
  212. render: () => h(Child)
  213. }
  214. render(h(Parent), root)
  215. expect(serializeInner(root)).toBe(`<div>0</div>`)
  216. class UpdatedChild {
  217. static __vccOpts: ComponentOptions = {
  218. __hmrId: childId,
  219. data() {
  220. return { count: 1 }
  221. },
  222. mounted: mountSpy,
  223. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  224. }
  225. }
  226. reload(childId, UpdatedChild)
  227. await nextTick()
  228. expect(serializeInner(root)).toBe(`<div>1</div>`)
  229. expect(unmountSpy).toHaveBeenCalledTimes(1)
  230. expect(mountSpy).toHaveBeenCalledTimes(1)
  231. })
  232. // #1156 - static nodes should retain DOM element reference across updates
  233. // when HMR is active
  234. test('static el reference', async () => {
  235. const root = nodeOps.createElement('div')
  236. const id = 'test-static-el'
  237. const template = `<div>
  238. <div>{{ count }}</div>
  239. <button @click="count++">++</button>
  240. </div>`
  241. const Comp: ComponentOptions = {
  242. __hmrId: id,
  243. data() {
  244. return { count: 0 }
  245. },
  246. render: compileToFunction(template)
  247. }
  248. createRecord(id, Comp)
  249. render(h(Comp), root)
  250. expect(serializeInner(root)).toBe(
  251. `<div><div>0</div><button>++</button></div>`
  252. )
  253. // 1. click to trigger update
  254. triggerEvent((root as any).children[0].children[1], 'click')
  255. await nextTick()
  256. expect(serializeInner(root)).toBe(
  257. `<div><div>1</div><button>++</button></div>`
  258. )
  259. // 2. trigger HMR
  260. rerender(
  261. id,
  262. compileToFunction(template.replace(`<button`, `<button class="foo"`))
  263. )
  264. expect(serializeInner(root)).toBe(
  265. `<div><div>1</div><button class="foo">++</button></div>`
  266. )
  267. })
  268. // #1157 - component should force full props update when HMR is active
  269. test('force update child component w/ static props', () => {
  270. const root = nodeOps.createElement('div')
  271. const parentId = 'test-force-props-parent'
  272. const childId = 'test-force-props-child'
  273. const Child: ComponentOptions = {
  274. __hmrId: childId,
  275. props: {
  276. msg: String
  277. },
  278. render: compileToFunction(`<div>{{ msg }}</div>`)
  279. }
  280. createRecord(childId, Child)
  281. const Parent: ComponentOptions = {
  282. __hmrId: parentId,
  283. components: { Child },
  284. render: compileToFunction(`<Child msg="foo" />`)
  285. }
  286. createRecord(parentId, Parent)
  287. render(h(Parent), root)
  288. expect(serializeInner(root)).toBe(`<div>foo</div>`)
  289. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  290. expect(serializeInner(root)).toBe(`<div>bar</div>`)
  291. })
  292. // #1305 - component should remove class
  293. test('remove static class from parent', () => {
  294. const root = nodeOps.createElement('div')
  295. const parentId = 'test-force-class-parent'
  296. const childId = 'test-force-class-child'
  297. const Child: ComponentOptions = {
  298. __hmrId: childId,
  299. render: compileToFunction(`<div>child</div>`)
  300. }
  301. createRecord(childId, Child)
  302. const Parent: ComponentOptions = {
  303. __hmrId: parentId,
  304. components: { Child },
  305. render: compileToFunction(`<Child class="test" />`)
  306. }
  307. createRecord(parentId, Parent)
  308. render(h(Parent), root)
  309. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  310. rerender(parentId, compileToFunction(`<Child/>`))
  311. expect(serializeInner(root)).toBe(`<div>child</div>`)
  312. })
  313. test('rerender if any parent in the parent chain', () => {
  314. const root = nodeOps.createElement('div')
  315. const parent = 'test-force-props-parent-'
  316. const childId = 'test-force-props-child'
  317. const numberOfParents = 5
  318. const Child: ComponentOptions = {
  319. __hmrId: childId,
  320. render: compileToFunction(`<div>child</div>`)
  321. }
  322. createRecord(childId, Child)
  323. const components: ComponentOptions[] = []
  324. for (let i = 0; i < numberOfParents; i++) {
  325. const parentId = `${parent}${i}`
  326. const parentComp: ComponentOptions = {
  327. __hmrId: parentId
  328. }
  329. components.push(parentComp)
  330. if (i === 0) {
  331. parentComp.render = compileToFunction(`<Child />`)
  332. parentComp.components = {
  333. Child
  334. }
  335. } else {
  336. parentComp.render = compileToFunction(`<Parent />`)
  337. parentComp.components = {
  338. Parent: components[i - 1]
  339. }
  340. }
  341. createRecord(parentId, parentComp)
  342. }
  343. const last = components[components.length - 1]
  344. render(h(last), root)
  345. expect(serializeInner(root)).toBe(`<div>child</div>`)
  346. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  347. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  348. })
  349. // #3302
  350. test('rerender with Teleport', () => {
  351. const root = nodeOps.createElement('div')
  352. const target = nodeOps.createElement('div')
  353. const parentId = 'parent-teleport'
  354. const Child: ComponentOptions = {
  355. data() {
  356. return {
  357. // style is used to ensure that the div tag will be tracked by Teleport
  358. style: {},
  359. target
  360. }
  361. },
  362. render: compileToFunction(`
  363. <teleport :to="target">
  364. <div :style="style">
  365. <slot/>
  366. </div>
  367. </teleport>
  368. `)
  369. }
  370. const Parent: ComponentOptions = {
  371. __hmrId: parentId,
  372. components: { Child },
  373. render: compileToFunction(`
  374. <Child>
  375. <template #default>
  376. <div>1</div>
  377. </template>
  378. </Child>
  379. `)
  380. }
  381. createRecord(parentId, Parent)
  382. render(h(Parent), root)
  383. expect(serializeInner(root)).toBe(
  384. `<!--teleport start--><!--teleport end-->`
  385. )
  386. expect(serializeInner(target)).toBe(`<div style={}><div>1</div></div>`)
  387. rerender(
  388. parentId,
  389. compileToFunction(`
  390. <Child>
  391. <template #default>
  392. <div>1</div>
  393. <div>2</div>
  394. </template>
  395. </Child>
  396. `)
  397. )
  398. expect(serializeInner(root)).toBe(
  399. `<!--teleport start--><!--teleport end-->`
  400. )
  401. expect(serializeInner(target)).toBe(
  402. `<div style={}><div>1</div><div>2</div></div>`
  403. )
  404. })
  405. // #4174
  406. test('with global mixins', async () => {
  407. const childId = 'hmr-global-mixin'
  408. const createSpy1 = vi.fn()
  409. const createSpy2 = vi.fn()
  410. const Child: ComponentOptions = {
  411. __hmrId: childId,
  412. created: createSpy1,
  413. render() {
  414. return h('div')
  415. }
  416. }
  417. createRecord(childId, Child)
  418. const Parent: ComponentOptions = {
  419. render: () => h(Child)
  420. }
  421. const app = createApp(Parent)
  422. app.mixin({})
  423. const root = nodeOps.createElement('div')
  424. app.mount(root)
  425. expect(createSpy1).toHaveBeenCalledTimes(1)
  426. expect(createSpy2).toHaveBeenCalledTimes(0)
  427. reload(childId, {
  428. __hmrId: childId,
  429. created: createSpy2,
  430. render() {
  431. return h('div')
  432. }
  433. })
  434. await nextTick()
  435. expect(createSpy1).toHaveBeenCalledTimes(1)
  436. expect(createSpy2).toHaveBeenCalledTimes(1)
  437. })
  438. // #4757
  439. test('rerender for component that has no active instance yet', () => {
  440. const id = 'no-active-instance-rerender'
  441. const Foo: ComponentOptions = {
  442. __hmrId: id,
  443. render: () => 'foo'
  444. }
  445. createRecord(id, Foo)
  446. rerender(id, () => 'bar')
  447. const root = nodeOps.createElement('div')
  448. render(h(Foo), root)
  449. expect(serializeInner(root)).toBe('bar')
  450. })
  451. test('reload for component that has no active instance yet', () => {
  452. const id = 'no-active-instance-reload'
  453. const Foo: ComponentOptions = {
  454. __hmrId: id,
  455. render: () => 'foo'
  456. }
  457. createRecord(id, Foo)
  458. reload(id, {
  459. __hmrId: id,
  460. render: () => 'bar'
  461. })
  462. const root = nodeOps.createElement('div')
  463. render(h(Foo), root)
  464. expect(serializeInner(root)).toBe('bar')
  465. })
  466. })