hmr.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. import { HMRRuntime } from '../src/hmr'
  2. import '../src/hmr'
  3. import { ComponentOptions, InternalRenderFunction } from '../src/component'
  4. import {
  5. render,
  6. nodeOps,
  7. h,
  8. serializeInner,
  9. triggerEvent,
  10. TestElement,
  11. nextTick
  12. } from '@vue/runtime-test'
  13. import * as runtimeTest from '@vue/runtime-test'
  14. import { registerRuntimeCompiler, createApp } from '@vue/runtime-test'
  15. import { baseCompile } from '@vue/compiler-core'
  16. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  17. const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
  18. registerRuntimeCompiler(compileToFunction)
  19. function compileToFunction(template: string) {
  20. const { code } = baseCompile(template)
  21. const render = new Function('Vue', code)(
  22. runtimeTest
  23. ) as InternalRenderFunction
  24. render._rc = true // isRuntimeCompiled
  25. return render
  26. }
  27. describe('hot module replacement', () => {
  28. test('inject global runtime', () => {
  29. expect(createRecord).toBeDefined()
  30. expect(rerender).toBeDefined()
  31. expect(reload).toBeDefined()
  32. })
  33. test('createRecord', () => {
  34. expect(createRecord('test1', {})).toBe(true)
  35. // if id has already been created, should return false
  36. expect(createRecord('test1', {})).toBe(false)
  37. })
  38. test('rerender', async () => {
  39. const root = nodeOps.createElement('div')
  40. const parentId = 'test2-parent'
  41. const childId = 'test2-child'
  42. const Child: ComponentOptions = {
  43. __hmrId: childId,
  44. render: compileToFunction(`<div><slot/></div>`)
  45. }
  46. createRecord(childId, Child)
  47. const Parent: ComponentOptions = {
  48. __hmrId: parentId,
  49. data() {
  50. return { count: 0 }
  51. },
  52. components: { Child },
  53. render: compileToFunction(
  54. `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
  55. )
  56. }
  57. createRecord(parentId, Parent)
  58. render(h(Parent), root)
  59. expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`)
  60. // Perform some state change. This change should be preserved after the
  61. // re-render!
  62. triggerEvent(root.children[0] as TestElement, 'click')
  63. await nextTick()
  64. expect(serializeInner(root)).toBe(`<div>1<div>1</div></div>`)
  65. // // Update text while preserving state
  66. rerender(
  67. parentId,
  68. compileToFunction(
  69. `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
  70. )
  71. )
  72. expect(serializeInner(root)).toBe(`<div>1!<div>1</div></div>`)
  73. // Should force child update on slot content change
  74. rerender(
  75. parentId,
  76. compileToFunction(
  77. `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`
  78. )
  79. )
  80. expect(serializeInner(root)).toBe(`<div>1!<div>1!</div></div>`)
  81. // Should force update element children despite block optimization
  82. rerender(
  83. parentId,
  84. compileToFunction(
  85. `<div @click="count++">{{ count }}<span>{{ count }}</span>
  86. <Child>{{ count }}!</Child>
  87. </div>`
  88. )
  89. )
  90. expect(serializeInner(root)).toBe(`<div>1<span>1</span><div>1!</div></div>`)
  91. // Should force update child slot elements
  92. rerender(
  93. parentId,
  94. compileToFunction(
  95. `<div @click="count++">
  96. <Child><span>{{ count }}</span></Child>
  97. </div>`
  98. )
  99. )
  100. expect(serializeInner(root)).toBe(`<div><div><span>1</span></div></div>`)
  101. })
  102. test('reload', async () => {
  103. const root = nodeOps.createElement('div')
  104. const childId = 'test3-child'
  105. const unmountSpy = jest.fn()
  106. const mountSpy = jest.fn()
  107. const Child: ComponentOptions = {
  108. __hmrId: childId,
  109. data() {
  110. return { count: 0 }
  111. },
  112. unmounted: unmountSpy,
  113. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  114. }
  115. createRecord(childId, Child)
  116. const Parent: ComponentOptions = {
  117. render: () => h(Child)
  118. }
  119. render(h(Parent), root)
  120. expect(serializeInner(root)).toBe(`<div>0</div>`)
  121. reload(childId, {
  122. __hmrId: childId,
  123. data() {
  124. return { count: 1 }
  125. },
  126. mounted: mountSpy,
  127. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  128. })
  129. await nextTick()
  130. expect(serializeInner(root)).toBe(`<div>1</div>`)
  131. expect(unmountSpy).toHaveBeenCalledTimes(1)
  132. expect(mountSpy).toHaveBeenCalledTimes(1)
  133. })
  134. // #7042
  135. test('reload KeepAlive slot', async () => {
  136. const root = nodeOps.createElement('div')
  137. const childId = 'test-child-keep-alive'
  138. const unmountSpy = jest.fn()
  139. const mountSpy = jest.fn()
  140. const activeSpy = jest.fn()
  141. const deactiveSpy = jest.fn()
  142. const Child: ComponentOptions = {
  143. __hmrId: childId,
  144. data() {
  145. return { count: 0 }
  146. },
  147. unmounted: unmountSpy,
  148. render: compileToFunction(`<div>{{ count }}</div>`)
  149. }
  150. createRecord(childId, Child)
  151. const Parent: ComponentOptions = {
  152. components: { Child },
  153. data() {
  154. return { toggle: true }
  155. },
  156. render: compileToFunction(
  157. `<button @click="toggle = !toggle"></button><KeepAlive><Child v-if="toggle" /></KeepAlive>`
  158. )
  159. }
  160. render(h(Parent), root)
  161. expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
  162. reload(childId, {
  163. __hmrId: childId,
  164. data() {
  165. return { count: 1 }
  166. },
  167. mounted: mountSpy,
  168. unmounted: unmountSpy,
  169. activated: activeSpy,
  170. deactivated: deactiveSpy,
  171. render: compileToFunction(`<div>{{ count }}</div>`)
  172. })
  173. await nextTick()
  174. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  175. expect(unmountSpy).toHaveBeenCalledTimes(1)
  176. expect(mountSpy).toHaveBeenCalledTimes(1)
  177. expect(activeSpy).toHaveBeenCalledTimes(1)
  178. expect(deactiveSpy).toHaveBeenCalledTimes(0)
  179. // should not unmount when toggling
  180. triggerEvent(root.children[1] as TestElement, 'click')
  181. await nextTick()
  182. expect(unmountSpy).toHaveBeenCalledTimes(1)
  183. expect(mountSpy).toHaveBeenCalledTimes(1)
  184. expect(activeSpy).toHaveBeenCalledTimes(1)
  185. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  186. // should not mount when toggling
  187. triggerEvent(root.children[1] as TestElement, 'click')
  188. await nextTick()
  189. expect(unmountSpy).toHaveBeenCalledTimes(1)
  190. expect(mountSpy).toHaveBeenCalledTimes(1)
  191. expect(activeSpy).toHaveBeenCalledTimes(2)
  192. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  193. })
  194. test('reload class component', async () => {
  195. const root = nodeOps.createElement('div')
  196. const childId = 'test4-child'
  197. const unmountSpy = jest.fn()
  198. const mountSpy = jest.fn()
  199. class Child {
  200. static __vccOpts: ComponentOptions = {
  201. __hmrId: childId,
  202. data() {
  203. return { count: 0 }
  204. },
  205. unmounted: unmountSpy,
  206. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  207. }
  208. }
  209. createRecord(childId, Child)
  210. const Parent: ComponentOptions = {
  211. render: () => h(Child)
  212. }
  213. render(h(Parent), root)
  214. expect(serializeInner(root)).toBe(`<div>0</div>`)
  215. class UpdatedChild {
  216. static __vccOpts: ComponentOptions = {
  217. __hmrId: childId,
  218. data() {
  219. return { count: 1 }
  220. },
  221. mounted: mountSpy,
  222. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  223. }
  224. }
  225. reload(childId, UpdatedChild)
  226. await nextTick()
  227. expect(serializeInner(root)).toBe(`<div>1</div>`)
  228. expect(unmountSpy).toHaveBeenCalledTimes(1)
  229. expect(mountSpy).toHaveBeenCalledTimes(1)
  230. })
  231. // #1156 - static nodes should retain DOM element reference across updates
  232. // when HMR is active
  233. test('static el reference', async () => {
  234. const root = nodeOps.createElement('div')
  235. const id = 'test-static-el'
  236. const template = `<div>
  237. <div>{{ count }}</div>
  238. <button @click="count++">++</button>
  239. </div>`
  240. const Comp: ComponentOptions = {
  241. __hmrId: id,
  242. data() {
  243. return { count: 0 }
  244. },
  245. render: compileToFunction(template)
  246. }
  247. createRecord(id, Comp)
  248. render(h(Comp), root)
  249. expect(serializeInner(root)).toBe(
  250. `<div><div>0</div><button>++</button></div>`
  251. )
  252. // 1. click to trigger update
  253. triggerEvent((root as any).children[0].children[1], 'click')
  254. await nextTick()
  255. expect(serializeInner(root)).toBe(
  256. `<div><div>1</div><button>++</button></div>`
  257. )
  258. // 2. trigger HMR
  259. rerender(
  260. id,
  261. compileToFunction(template.replace(`<button`, `<button class="foo"`))
  262. )
  263. expect(serializeInner(root)).toBe(
  264. `<div><div>1</div><button class="foo">++</button></div>`
  265. )
  266. })
  267. // #1157 - component should force full props update when HMR is active
  268. test('force update child component w/ static props', () => {
  269. const root = nodeOps.createElement('div')
  270. const parentId = 'test-force-props-parent'
  271. const childId = 'test-force-props-child'
  272. const Child: ComponentOptions = {
  273. __hmrId: childId,
  274. props: {
  275. msg: String
  276. },
  277. render: compileToFunction(`<div>{{ msg }}</div>`)
  278. }
  279. createRecord(childId, Child)
  280. const Parent: ComponentOptions = {
  281. __hmrId: parentId,
  282. components: { Child },
  283. render: compileToFunction(`<Child msg="foo" />`)
  284. }
  285. createRecord(parentId, Parent)
  286. render(h(Parent), root)
  287. expect(serializeInner(root)).toBe(`<div>foo</div>`)
  288. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  289. expect(serializeInner(root)).toBe(`<div>bar</div>`)
  290. })
  291. // #1305 - component should remove class
  292. test('remove static class from parent', () => {
  293. const root = nodeOps.createElement('div')
  294. const parentId = 'test-force-class-parent'
  295. const childId = 'test-force-class-child'
  296. const Child: ComponentOptions = {
  297. __hmrId: childId,
  298. render: compileToFunction(`<div>child</div>`)
  299. }
  300. createRecord(childId, Child)
  301. const Parent: ComponentOptions = {
  302. __hmrId: parentId,
  303. components: { Child },
  304. render: compileToFunction(`<Child class="test" />`)
  305. }
  306. createRecord(parentId, Parent)
  307. render(h(Parent), root)
  308. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  309. rerender(parentId, compileToFunction(`<Child/>`))
  310. expect(serializeInner(root)).toBe(`<div>child</div>`)
  311. })
  312. test('rerender if any parent in the parent chain', () => {
  313. const root = nodeOps.createElement('div')
  314. const parent = 'test-force-props-parent-'
  315. const childId = 'test-force-props-child'
  316. const numberOfParents = 5
  317. const Child: ComponentOptions = {
  318. __hmrId: childId,
  319. render: compileToFunction(`<div>child</div>`)
  320. }
  321. createRecord(childId, Child)
  322. const components: ComponentOptions[] = []
  323. for (let i = 0; i < numberOfParents; i++) {
  324. const parentId = `${parent}${i}`
  325. const parentComp: ComponentOptions = {
  326. __hmrId: parentId
  327. }
  328. components.push(parentComp)
  329. if (i === 0) {
  330. parentComp.render = compileToFunction(`<Child />`)
  331. parentComp.components = {
  332. Child
  333. }
  334. } else {
  335. parentComp.render = compileToFunction(`<Parent />`)
  336. parentComp.components = {
  337. Parent: components[i - 1]
  338. }
  339. }
  340. createRecord(parentId, parentComp)
  341. }
  342. const last = components[components.length - 1]
  343. render(h(last), root)
  344. expect(serializeInner(root)).toBe(`<div>child</div>`)
  345. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  346. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  347. })
  348. // #3302
  349. test('rerender with Teleport', () => {
  350. const root = nodeOps.createElement('div')
  351. const target = nodeOps.createElement('div')
  352. const parentId = 'parent-teleport'
  353. const Child: ComponentOptions = {
  354. data() {
  355. return {
  356. // style is used to ensure that the div tag will be tracked by Teleport
  357. style: {},
  358. target
  359. }
  360. },
  361. render: compileToFunction(`
  362. <teleport :to="target">
  363. <div :style="style">
  364. <slot/>
  365. </div>
  366. </teleport>
  367. `)
  368. }
  369. const Parent: ComponentOptions = {
  370. __hmrId: parentId,
  371. components: { Child },
  372. render: compileToFunction(`
  373. <Child>
  374. <template #default>
  375. <div>1</div>
  376. </template>
  377. </Child>
  378. `)
  379. }
  380. createRecord(parentId, Parent)
  381. render(h(Parent), root)
  382. expect(serializeInner(root)).toBe(
  383. `<!--teleport start--><!--teleport end-->`
  384. )
  385. expect(serializeInner(target)).toBe(`<div style={}><div>1</div></div>`)
  386. rerender(
  387. parentId,
  388. compileToFunction(`
  389. <Child>
  390. <template #default>
  391. <div>1</div>
  392. <div>2</div>
  393. </template>
  394. </Child>
  395. `)
  396. )
  397. expect(serializeInner(root)).toBe(
  398. `<!--teleport start--><!--teleport end-->`
  399. )
  400. expect(serializeInner(target)).toBe(
  401. `<div style={}><div>1</div><div>2</div></div>`
  402. )
  403. })
  404. // #4174
  405. test('with global mixins', async () => {
  406. const childId = 'hmr-global-mixin'
  407. const createSpy1 = jest.fn()
  408. const createSpy2 = jest.fn()
  409. const Child: ComponentOptions = {
  410. __hmrId: childId,
  411. created: createSpy1,
  412. render() {
  413. return h('div')
  414. }
  415. }
  416. createRecord(childId, Child)
  417. const Parent: ComponentOptions = {
  418. render: () => h(Child)
  419. }
  420. const app = createApp(Parent)
  421. app.mixin({})
  422. const root = nodeOps.createElement('div')
  423. app.mount(root)
  424. expect(createSpy1).toHaveBeenCalledTimes(1)
  425. expect(createSpy2).toHaveBeenCalledTimes(0)
  426. reload(childId, {
  427. __hmrId: childId,
  428. created: createSpy2,
  429. render() {
  430. return h('div')
  431. }
  432. })
  433. await nextTick()
  434. expect(createSpy1).toHaveBeenCalledTimes(1)
  435. expect(createSpy2).toHaveBeenCalledTimes(1)
  436. })
  437. // #4757
  438. test('rerender for component that has no active instance yet', () => {
  439. const id = 'no-active-instance-rerender'
  440. const Foo: ComponentOptions = {
  441. __hmrId: id,
  442. render: () => 'foo'
  443. }
  444. createRecord(id, Foo)
  445. rerender(id, () => 'bar')
  446. const root = nodeOps.createElement('div')
  447. render(h(Foo), root)
  448. expect(serializeInner(root)).toBe('bar')
  449. })
  450. test('reload for component that has no active instance yet', () => {
  451. const id = 'no-active-instance-reload'
  452. const Foo: ComponentOptions = {
  453. __hmrId: id,
  454. render: () => 'foo'
  455. }
  456. createRecord(id, Foo)
  457. reload(id, {
  458. __hmrId: id,
  459. render: () => 'bar'
  460. })
  461. const root = nodeOps.createElement('div')
  462. render(h(Foo), root)
  463. expect(serializeInner(root)).toBe('bar')
  464. })
  465. })