hmr.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  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, { hoistStatic: true, hmr: true })
  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 = vi.fn()
  106. const mountSpy = vi.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 = vi.fn()
  139. const mountSpy = vi.fn()
  140. const activeSpy = vi.fn()
  141. const deactiveSpy = vi.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. // #7121
  195. test('reload KeepAlive slot in Transition', async () => {
  196. const root = nodeOps.createElement('div')
  197. const childId = 'test-transition-keep-alive-reload'
  198. const unmountSpy = vi.fn()
  199. const mountSpy = vi.fn()
  200. const activeSpy = vi.fn()
  201. const deactiveSpy = vi.fn()
  202. const Child: ComponentOptions = {
  203. __hmrId: childId,
  204. data() {
  205. return { count: 0 }
  206. },
  207. unmounted: unmountSpy,
  208. render: compileToFunction(`<div>{{ count }}</div>`)
  209. }
  210. createRecord(childId, Child)
  211. const Parent: ComponentOptions = {
  212. components: { Child },
  213. data() {
  214. return { toggle: true }
  215. },
  216. render: compileToFunction(
  217. `<button @click="toggle = !toggle"></button><BaseTransition mode="out-in"><KeepAlive><Child v-if="toggle" /></KeepAlive></BaseTransition>`
  218. )
  219. }
  220. render(h(Parent), root)
  221. expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
  222. reload(childId, {
  223. __hmrId: childId,
  224. data() {
  225. return { count: 1 }
  226. },
  227. mounted: mountSpy,
  228. unmounted: unmountSpy,
  229. activated: activeSpy,
  230. deactivated: deactiveSpy,
  231. render: compileToFunction(`<div>{{ count }}</div>`)
  232. })
  233. await nextTick()
  234. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  235. expect(unmountSpy).toHaveBeenCalledTimes(1)
  236. expect(mountSpy).toHaveBeenCalledTimes(1)
  237. expect(activeSpy).toHaveBeenCalledTimes(1)
  238. expect(deactiveSpy).toHaveBeenCalledTimes(0)
  239. // should not unmount when toggling
  240. triggerEvent(root.children[1] as TestElement, 'click')
  241. await nextTick()
  242. expect(serializeInner(root)).toBe(`<button></button><!---->`)
  243. expect(unmountSpy).toHaveBeenCalledTimes(1)
  244. expect(mountSpy).toHaveBeenCalledTimes(1)
  245. expect(activeSpy).toHaveBeenCalledTimes(1)
  246. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  247. // should not mount when toggling
  248. triggerEvent(root.children[1] as TestElement, 'click')
  249. await nextTick()
  250. expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
  251. expect(unmountSpy).toHaveBeenCalledTimes(1)
  252. expect(mountSpy).toHaveBeenCalledTimes(1)
  253. expect(activeSpy).toHaveBeenCalledTimes(2)
  254. expect(deactiveSpy).toHaveBeenCalledTimes(1)
  255. })
  256. test('reload class component', async () => {
  257. const root = nodeOps.createElement('div')
  258. const childId = 'test4-child'
  259. const unmountSpy = vi.fn()
  260. const mountSpy = vi.fn()
  261. class Child {
  262. static __vccOpts: ComponentOptions = {
  263. __hmrId: childId,
  264. data() {
  265. return { count: 0 }
  266. },
  267. unmounted: unmountSpy,
  268. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  269. }
  270. }
  271. createRecord(childId, Child)
  272. const Parent: ComponentOptions = {
  273. render: () => h(Child)
  274. }
  275. render(h(Parent), root)
  276. expect(serializeInner(root)).toBe(`<div>0</div>`)
  277. class UpdatedChild {
  278. static __vccOpts: ComponentOptions = {
  279. __hmrId: childId,
  280. data() {
  281. return { count: 1 }
  282. },
  283. mounted: mountSpy,
  284. render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
  285. }
  286. }
  287. reload(childId, UpdatedChild)
  288. await nextTick()
  289. expect(serializeInner(root)).toBe(`<div>1</div>`)
  290. expect(unmountSpy).toHaveBeenCalledTimes(1)
  291. expect(mountSpy).toHaveBeenCalledTimes(1)
  292. })
  293. // #1156 - static nodes should retain DOM element reference across updates
  294. // when HMR is active
  295. test('static el reference', async () => {
  296. const root = nodeOps.createElement('div')
  297. const id = 'test-static-el'
  298. const template = `<div>
  299. <div>{{ count }}</div>
  300. <button @click="count++">++</button>
  301. </div>`
  302. const Comp: ComponentOptions = {
  303. __hmrId: id,
  304. data() {
  305. return { count: 0 }
  306. },
  307. render: compileToFunction(template)
  308. }
  309. createRecord(id, Comp)
  310. render(h(Comp), root)
  311. expect(serializeInner(root)).toBe(
  312. `<div><div>0</div><button>++</button></div>`
  313. )
  314. // 1. click to trigger update
  315. triggerEvent((root as any).children[0].children[1], 'click')
  316. await nextTick()
  317. expect(serializeInner(root)).toBe(
  318. `<div><div>1</div><button>++</button></div>`
  319. )
  320. // 2. trigger HMR
  321. rerender(
  322. id,
  323. compileToFunction(template.replace(`<button`, `<button class="foo"`))
  324. )
  325. expect(serializeInner(root)).toBe(
  326. `<div><div>1</div><button class="foo">++</button></div>`
  327. )
  328. })
  329. // #1157 - component should force full props update when HMR is active
  330. test('force update child component w/ static props', () => {
  331. const root = nodeOps.createElement('div')
  332. const parentId = 'test-force-props-parent'
  333. const childId = 'test-force-props-child'
  334. const Child: ComponentOptions = {
  335. __hmrId: childId,
  336. props: {
  337. msg: String
  338. },
  339. render: compileToFunction(`<div>{{ msg }}</div>`)
  340. }
  341. createRecord(childId, Child)
  342. const Parent: ComponentOptions = {
  343. __hmrId: parentId,
  344. components: { Child },
  345. render: compileToFunction(`<Child msg="foo" />`)
  346. }
  347. createRecord(parentId, Parent)
  348. render(h(Parent), root)
  349. expect(serializeInner(root)).toBe(`<div>foo</div>`)
  350. rerender(parentId, compileToFunction(`<Child msg="bar" />`))
  351. expect(serializeInner(root)).toBe(`<div>bar</div>`)
  352. })
  353. // #1305 - component should remove class
  354. test('remove static class from parent', () => {
  355. const root = nodeOps.createElement('div')
  356. const parentId = 'test-force-class-parent'
  357. const childId = 'test-force-class-child'
  358. const Child: ComponentOptions = {
  359. __hmrId: childId,
  360. render: compileToFunction(`<div>child</div>`)
  361. }
  362. createRecord(childId, Child)
  363. const Parent: ComponentOptions = {
  364. __hmrId: parentId,
  365. components: { Child },
  366. render: compileToFunction(`<Child class="test" />`)
  367. }
  368. createRecord(parentId, Parent)
  369. render(h(Parent), root)
  370. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  371. rerender(parentId, compileToFunction(`<Child/>`))
  372. expect(serializeInner(root)).toBe(`<div>child</div>`)
  373. })
  374. test('rerender if any parent in the parent chain', () => {
  375. const root = nodeOps.createElement('div')
  376. const parent = 'test-force-props-parent-'
  377. const childId = 'test-force-props-child'
  378. const numberOfParents = 5
  379. const Child: ComponentOptions = {
  380. __hmrId: childId,
  381. render: compileToFunction(`<div>child</div>`)
  382. }
  383. createRecord(childId, Child)
  384. const components: ComponentOptions[] = []
  385. for (let i = 0; i < numberOfParents; i++) {
  386. const parentId = `${parent}${i}`
  387. const parentComp: ComponentOptions = {
  388. __hmrId: parentId
  389. }
  390. components.push(parentComp)
  391. if (i === 0) {
  392. parentComp.render = compileToFunction(`<Child />`)
  393. parentComp.components = {
  394. Child
  395. }
  396. } else {
  397. parentComp.render = compileToFunction(`<Parent />`)
  398. parentComp.components = {
  399. Parent: components[i - 1]
  400. }
  401. }
  402. createRecord(parentId, parentComp)
  403. }
  404. const last = components[components.length - 1]
  405. render(h(last), root)
  406. expect(serializeInner(root)).toBe(`<div>child</div>`)
  407. rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`))
  408. expect(serializeInner(root)).toBe(`<div class="test">child</div>`)
  409. })
  410. // #3302
  411. test('rerender with Teleport', () => {
  412. const root = nodeOps.createElement('div')
  413. const target = nodeOps.createElement('div')
  414. const parentId = 'parent-teleport'
  415. const Child: ComponentOptions = {
  416. data() {
  417. return {
  418. // style is used to ensure that the div tag will be tracked by Teleport
  419. style: {},
  420. target
  421. }
  422. },
  423. render: compileToFunction(`
  424. <teleport :to="target">
  425. <div :style="style">
  426. <slot/>
  427. </div>
  428. </teleport>
  429. `)
  430. }
  431. const Parent: ComponentOptions = {
  432. __hmrId: parentId,
  433. components: { Child },
  434. render: compileToFunction(`
  435. <Child>
  436. <template #default>
  437. <div>1</div>
  438. </template>
  439. </Child>
  440. `)
  441. }
  442. createRecord(parentId, Parent)
  443. render(h(Parent), root)
  444. expect(serializeInner(root)).toBe(
  445. `<!--teleport start--><!--teleport end-->`
  446. )
  447. expect(serializeInner(target)).toBe(`<div style={}><div>1</div></div>`)
  448. rerender(
  449. parentId,
  450. compileToFunction(`
  451. <Child>
  452. <template #default>
  453. <div>1</div>
  454. <div>2</div>
  455. </template>
  456. </Child>
  457. `)
  458. )
  459. expect(serializeInner(root)).toBe(
  460. `<!--teleport start--><!--teleport end-->`
  461. )
  462. expect(serializeInner(target)).toBe(
  463. `<div style={}><div>1</div><div>2</div></div>`
  464. )
  465. })
  466. // #4174
  467. test('with global mixins', async () => {
  468. const childId = 'hmr-global-mixin'
  469. const createSpy1 = vi.fn()
  470. const createSpy2 = vi.fn()
  471. const Child: ComponentOptions = {
  472. __hmrId: childId,
  473. created: createSpy1,
  474. render() {
  475. return h('div')
  476. }
  477. }
  478. createRecord(childId, Child)
  479. const Parent: ComponentOptions = {
  480. render: () => h(Child)
  481. }
  482. const app = createApp(Parent)
  483. app.mixin({})
  484. const root = nodeOps.createElement('div')
  485. app.mount(root)
  486. expect(createSpy1).toHaveBeenCalledTimes(1)
  487. expect(createSpy2).toHaveBeenCalledTimes(0)
  488. reload(childId, {
  489. __hmrId: childId,
  490. created: createSpy2,
  491. render() {
  492. return h('div')
  493. }
  494. })
  495. await nextTick()
  496. expect(createSpy1).toHaveBeenCalledTimes(1)
  497. expect(createSpy2).toHaveBeenCalledTimes(1)
  498. })
  499. // #4757
  500. test('rerender for component that has no active instance yet', () => {
  501. const id = 'no-active-instance-rerender'
  502. const Foo: ComponentOptions = {
  503. __hmrId: id,
  504. render: () => 'foo'
  505. }
  506. createRecord(id, Foo)
  507. rerender(id, () => 'bar')
  508. const root = nodeOps.createElement('div')
  509. render(h(Foo), root)
  510. expect(serializeInner(root)).toBe('bar')
  511. })
  512. test('reload for component that has no active instance yet', () => {
  513. const id = 'no-active-instance-reload'
  514. const Foo: ComponentOptions = {
  515. __hmrId: id,
  516. render: () => 'foo'
  517. }
  518. createRecord(id, Foo)
  519. reload(id, {
  520. __hmrId: id,
  521. render: () => 'bar'
  522. })
  523. const root = nodeOps.createElement('div')
  524. render(h(Foo), root)
  525. expect(serializeInner(root)).toBe('bar')
  526. })
  527. // #7155 - force HMR on slots content update
  528. test('force update slot content change', () => {
  529. const root = nodeOps.createElement('div')
  530. const parentId = 'test-force-computed-parent'
  531. const childId = 'test-force-computed-child'
  532. const Child: ComponentOptions = {
  533. __hmrId: childId,
  534. computed: {
  535. slotContent() {
  536. return this.$slots.default?.()
  537. }
  538. },
  539. render: compileToFunction(`<component :is="() => slotContent" />`)
  540. }
  541. createRecord(childId, Child)
  542. const Parent: ComponentOptions = {
  543. __hmrId: parentId,
  544. components: { Child },
  545. render: compileToFunction(`<Child>1</Child>`)
  546. }
  547. createRecord(parentId, Parent)
  548. render(h(Parent), root)
  549. expect(serializeInner(root)).toBe(`1`)
  550. rerender(parentId, compileToFunction(`<Child>2</Child>`))
  551. expect(serializeInner(root)).toBe(`2`)
  552. })
  553. // #6978, #7138, #7114
  554. test('hoisted children array inside v-for', () => {
  555. const root = nodeOps.createElement('div')
  556. const appId = 'test-app-id'
  557. const App: ComponentOptions = {
  558. __hmrId: appId,
  559. render: compileToFunction(
  560. `<div v-for="item of 2">
  561. <div>1</div>
  562. </div>
  563. <p>2</p>
  564. <p>3</p>`
  565. )
  566. }
  567. createRecord(appId, App)
  568. render(h(App), root)
  569. expect(serializeInner(root)).toBe(
  570. `<div><div>1</div></div><div><div>1</div></div><p>2</p><p>3</p>`
  571. )
  572. // move the <p>3</p> into the <div>1</div>
  573. rerender(
  574. appId,
  575. compileToFunction(
  576. `<div v-for="item of 2">
  577. <div>1<p>3</p></div>
  578. </div>
  579. <p>2</p>`
  580. )
  581. )
  582. expect(serializeInner(root)).toBe(
  583. `<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`
  584. )
  585. })
  586. })