hmr.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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. test('reload class component', async () => {
  135. const root = nodeOps.createElement('div')
  136. const childId = 'test4-child'
  137. const unmountSpy = jest.fn()
  138. const mountSpy = jest.fn()
  139. class Child {
  140. static __vccOpts: ComponentOptions = {
  141. __hmrId: childId,
  142. data() {
  143. return { count: 0 }
  144. },
  145. unmounted: unmountSpy,
  146. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  147. }
  148. }
  149. createRecord(childId, Child)
  150. const Parent: ComponentOptions = {
  151. render: () => h(Child)
  152. }
  153. render(h(Parent), root)
  154. expect(serializeInner(root)).toBe(`<div>0</div>`)
  155. class UpdatedChild {
  156. static __vccOpts: ComponentOptions = {
  157. __hmrId: childId,
  158. data() {
  159. return { count: 1 }
  160. },
  161. mounted: mountSpy,
  162. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  163. }
  164. }
  165. reload(childId, UpdatedChild)
  166. await nextTick()
  167. expect(serializeInner(root)).toBe(`<div>1</div>`)
  168. expect(unmountSpy).toHaveBeenCalledTimes(1)
  169. expect(mountSpy).toHaveBeenCalledTimes(1)
  170. })
  171. // #1156 - static nodes should retain DOM element reference across updates
  172. // when HMR is active
  173. test('static el reference', async () => {
  174. const root = nodeOps.createElement('div')
  175. const id = 'test-static-el'
  176. const template = `<div>
  177. <div>{{ count }}</div>
  178. <button @click="count++">++</button>
  179. </div>`
  180. const Comp: ComponentOptions = {
  181. __hmrId: id,
  182. data() {
  183. return { count: 0 }
  184. },
  185. render: compileToFunction(template)
  186. }
  187. createRecord(id, Comp)
  188. render(h(Comp), root)
  189. expect(serializeInner(root)).toBe(
  190. `<div><div>0</div><button>++</button></div>`
  191. )
  192. // 1. click to trigger update
  193. triggerEvent((root as any).children[0].children[1], 'click')
  194. await nextTick()
  195. expect(serializeInner(root)).toBe(
  196. `<div><div>1</div><button>++</button></div>`
  197. )
  198. // 2. trigger HMR
  199. rerender(
  200. id,
  201. compileToFunction(template.replace(`<button`, `<button class="foo"`))
  202. )
  203. expect(serializeInner(root)).toBe(
  204. `<div><div>1</div><button class="foo">++</button></div>`
  205. )
  206. })
  207. // #1157 - component should force full props update when HMR is active
  208. test('force update child component w/ static props', () => {
  209. const root = nodeOps.createElement('div')
  210. const parentId = 'test-force-props-parent'
  211. const childId = 'test-force-props-child'
  212. const Child: ComponentOptions = {
  213. __hmrId: childId,
  214. props: {
  215. msg: String
  216. },
  217. render: compileToFunction(`<div>{{ msg }}</div>`)
  218. }
  219. createRecord(childId, Child)
  220. const Parent: ComponentOptions = {
  221. __hmrId: parentId,
  222. components: { Child },
  223. render: compileToFunction(`<Child msg="foo" />`)
  224. }
  225. createRecord(parentId, Parent)
  226. render(h(Parent), root)
  227. expect(serializeInner(root)).toBe(`<div>foo</div>`)
  228. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  229. expect(serializeInner(root)).toBe(`<div>bar</div>`)
  230. })
  231. // #1305 - component should remove class
  232. test('remove static class from parent', () => {
  233. const root = nodeOps.createElement('div')
  234. const parentId = 'test-force-class-parent'
  235. const childId = 'test-force-class-child'
  236. const Child: ComponentOptions = {
  237. __hmrId: childId,
  238. render: compileToFunction(`<div>child</div>`)
  239. }
  240. createRecord(childId, Child)
  241. const Parent: ComponentOptions = {
  242. __hmrId: parentId,
  243. components: { Child },
  244. render: compileToFunction(`<Child class="test" />`)
  245. }
  246. createRecord(parentId, Parent)
  247. render(h(Parent), root)
  248. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  249. rerender(parentId, compileToFunction(`<Child/>`))
  250. expect(serializeInner(root)).toBe(`<div>child</div>`)
  251. })
  252. test('rerender if any parent in the parent chain', () => {
  253. const root = nodeOps.createElement('div')
  254. const parent = 'test-force-props-parent-'
  255. const childId = 'test-force-props-child'
  256. const numberOfParents = 5
  257. const Child: ComponentOptions = {
  258. __hmrId: childId,
  259. render: compileToFunction(`<div>child</div>`)
  260. }
  261. createRecord(childId, Child)
  262. const components: ComponentOptions[] = []
  263. for (let i = 0; i < numberOfParents; i++) {
  264. const parentId = `${parent}${i}`
  265. const parentComp: ComponentOptions = {
  266. __hmrId: parentId
  267. }
  268. components.push(parentComp)
  269. if (i === 0) {
  270. parentComp.render = compileToFunction(`<Child />`)
  271. parentComp.components = {
  272. Child
  273. }
  274. } else {
  275. parentComp.render = compileToFunction(`<Parent />`)
  276. parentComp.components = {
  277. Parent: components[i - 1]
  278. }
  279. }
  280. createRecord(parentId, parentComp)
  281. }
  282. const last = components[components.length - 1]
  283. render(h(last), root)
  284. expect(serializeInner(root)).toBe(`<div>child</div>`)
  285. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  286. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  287. })
  288. // #3302
  289. test('rerender with Teleport', () => {
  290. const root = nodeOps.createElement('div')
  291. const target = nodeOps.createElement('div')
  292. const parentId = 'parent-teleport'
  293. const Child: ComponentOptions = {
  294. data() {
  295. return {
  296. // style is used to ensure that the div tag will be tracked by Teleport
  297. style: {},
  298. target
  299. }
  300. },
  301. render: compileToFunction(`
  302. <teleport :to="target">
  303. <div :style="style">
  304. <slot/>
  305. </div>
  306. </teleport>
  307. `)
  308. }
  309. const Parent: ComponentOptions = {
  310. __hmrId: parentId,
  311. components: { Child },
  312. render: compileToFunction(`
  313. <Child>
  314. <template #default>
  315. <div>1</div>
  316. </template>
  317. </Child>
  318. `)
  319. }
  320. createRecord(parentId, Parent)
  321. render(h(Parent), root)
  322. expect(serializeInner(root)).toBe(
  323. `<!--teleport start--><!--teleport end-->`
  324. )
  325. expect(serializeInner(target)).toBe(`<div style={}><div>1</div></div>`)
  326. rerender(
  327. parentId,
  328. compileToFunction(`
  329. <Child>
  330. <template #default>
  331. <div>1</div>
  332. <div>2</div>
  333. </template>
  334. </Child>
  335. `)
  336. )
  337. expect(serializeInner(root)).toBe(
  338. `<!--teleport start--><!--teleport end-->`
  339. )
  340. expect(serializeInner(target)).toBe(
  341. `<div style={}><div>1</div><div>2</div></div>`
  342. )
  343. })
  344. // #4174
  345. test('with global mixins', async () => {
  346. const childId = 'hmr-global-mixin'
  347. const createSpy1 = jest.fn()
  348. const createSpy2 = jest.fn()
  349. const Child: ComponentOptions = {
  350. __hmrId: childId,
  351. created: createSpy1,
  352. render() {
  353. return h('div')
  354. }
  355. }
  356. createRecord(childId, Child)
  357. const Parent: ComponentOptions = {
  358. render: () => h(Child)
  359. }
  360. const app = createApp(Parent)
  361. app.mixin({})
  362. const root = nodeOps.createElement('div')
  363. app.mount(root)
  364. expect(createSpy1).toHaveBeenCalledTimes(1)
  365. expect(createSpy2).toHaveBeenCalledTimes(0)
  366. reload(childId, {
  367. __hmrId: childId,
  368. created: createSpy2,
  369. render() {
  370. return h('div')
  371. }
  372. })
  373. await nextTick()
  374. expect(createSpy1).toHaveBeenCalledTimes(1)
  375. expect(createSpy2).toHaveBeenCalledTimes(1)
  376. })
  377. // #4757
  378. test('rerender for component that has no active instance yet', () => {
  379. const id = 'no-active-instance-rerender'
  380. const Foo: ComponentOptions = {
  381. __hmrId: id,
  382. render: () => 'foo'
  383. }
  384. createRecord(id, Foo)
  385. rerender(id, () => 'bar')
  386. const root = nodeOps.createElement('div')
  387. render(h(Foo), root)
  388. expect(serializeInner(root)).toBe('bar')
  389. })
  390. test('reload for component that has no active instance yet', () => {
  391. const id = 'no-active-instance-reload'
  392. const Foo: ComponentOptions = {
  393. __hmrId: id,
  394. render: () => 'foo'
  395. }
  396. createRecord(id, Foo)
  397. reload(id, {
  398. __hmrId: id,
  399. render: () => 'bar'
  400. })
  401. const root = nodeOps.createElement('div')
  402. render(h(Foo), root)
  403. expect(serializeInner(root)).toBe('bar')
  404. })
  405. })