hmr.spec.ts 25 KB

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