import type { HMRRuntime } from '../src/hmr' import '../src/hmr' import type { ComponentOptions, InternalRenderFunction } from '../src/component' import { type TestElement, h, nextTick, nodeOps, ref, render, serializeInner, triggerEvent, } from '@vue/runtime-test' import * as runtimeTest from '@vue/runtime-test' import { createApp, registerRuntimeCompiler } from '@vue/runtime-test' import { baseCompile } from '@vue/compiler-core' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ registerRuntimeCompiler(compileToFunction) function compileToFunction(template: string) { const { code } = baseCompile(template, { hoistStatic: true, hmr: true }) const render = new Function('Vue', code)( runtimeTest, ) as InternalRenderFunction render._rc = true // isRuntimeCompiled return render } const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) describe('hot module replacement', () => { test('inject global runtime', () => { expect(createRecord).toBeDefined() expect(rerender).toBeDefined() expect(reload).toBeDefined() }) test('createRecord', () => { expect(createRecord('test1', {})).toBe(true) // if id has already been created, should return false expect(createRecord('test1', {})).toBe(false) }) test('rerender', async () => { const root = nodeOps.createElement('div') const parentId = 'test2-parent' const childId = 'test2-child' const Child: ComponentOptions = { __hmrId: childId, render: compileToFunction(`
`), } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, data() { return { count: 0 } }, components: { Child }, render: compileToFunction( `
{{ count }}{{ count }}
`, ), } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`
0
0
`) // Perform some state change. This change should be preserved after the // re-render! triggerEvent(root.children[0] as TestElement, 'click') await nextTick() expect(serializeInner(root)).toBe(`
1
1
`) // // Update text while preserving state rerender( parentId, compileToFunction( `
{{ count }}!{{ count }}
`, ), ) expect(serializeInner(root)).toBe(`
1!
1
`) // Should force child update on slot content change rerender( parentId, compileToFunction( `
{{ count }}!{{ count }}!
`, ), ) expect(serializeInner(root)).toBe(`
1!
1!
`) // Should force update element children despite block optimization rerender( parentId, compileToFunction( `
{{ count }}{{ count }} {{ count }}!
`, ), ) expect(serializeInner(root)).toBe(`
11
1!
`) // Should force update child slot elements rerender( parentId, compileToFunction( `
{{ count }}
`, ), ) expect(serializeInner(root)).toBe(`
1
`) }) test('reload', async () => { const root = nodeOps.createElement('div') const childId = 'test3-child' const unmountSpy = vi.fn() const mountSpy = vi.fn() const Child: ComponentOptions = { __hmrId: childId, data() { return { count: 0 } }, unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`), } createRecord(childId, Child) const Parent: ComponentOptions = { render: () => h(Child), } render(h(Parent), root) expect(serializeInner(root)).toBe(`
0
`) reload(childId, { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) }) // #7042 test('reload KeepAlive slot', async () => { const root = nodeOps.createElement('div') const childId = 'test-child-keep-alive' const unmountSpy = vi.fn() const mountSpy = vi.fn() const activeSpy = vi.fn() const deactiveSpy = vi.fn() const Child: ComponentOptions = { __hmrId: childId, data() { return { count: 0 } }, unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`), } createRecord(childId, Child) const Parent: ComponentOptions = { components: { Child }, data() { return { toggle: true } }, render: compileToFunction( `
0
`) reload(childId, { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, unmounted: unmountSpy, activated: activeSpy, deactivated: deactiveSpy, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling triggerEvent(root.children[1] as TestElement, 'click') await nextTick() expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(1) // should not mount when toggling triggerEvent(root.children[1] as TestElement, 'click') await nextTick() expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(2) expect(deactiveSpy).toHaveBeenCalledTimes(1) }) // #7121 test('reload KeepAlive slot in Transition', async () => { const root = nodeOps.createElement('div') const childId = 'test-transition-keep-alive-reload' const unmountSpy = vi.fn() const mountSpy = vi.fn() const activeSpy = vi.fn() const deactiveSpy = vi.fn() const Child: ComponentOptions = { __hmrId: childId, data() { return { count: 0 } }, unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`), } createRecord(childId, Child) const Parent: ComponentOptions = { components: { Child }, data() { return { toggle: true } }, render: compileToFunction( `
0
`) reload(childId, { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, unmounted: unmountSpy, activated: activeSpy, deactivated: deactiveSpy, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling triggerEvent(root.children[1] as TestElement, 'click') await nextTick() expect(serializeInner(root)).toBe(``) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(1) // should not mount when toggling triggerEvent(root.children[1] as TestElement, 'click') await nextTick() expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(2) expect(deactiveSpy).toHaveBeenCalledTimes(1) }) test('reload KeepAlive slot in Transition with out-in', async () => { const root = nodeOps.createElement('div') const childId = 'test-transition-keep-alive-reload-with-out-in' const unmountSpy = vi.fn() const mountSpy = vi.fn() const activeSpy = vi.fn() const deactiveSpy = vi.fn() const Child: ComponentOptions = { __hmrId: childId, name: 'original', data() { return { count: 0 } }, unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`), } createRecord(childId, Child) const Parent: ComponentOptions = { components: { Child }, data() { return { toggle: true } }, methods: { // @ts-expect-error onLeave(_, done) { setTimeout(done, 0) }, }, render: compileToFunction( `
0
`) reload(childId, { __hmrId: childId, name: 'updated', data() { return { count: 1 } }, mounted: mountSpy, unmounted: unmountSpy, activated: activeSpy, deactivated: deactiveSpy, render: compileToFunction(`
{{ count }}
`), }) await nextTick() await new Promise(r => setTimeout(r, 0)) expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling triggerEvent(root.children[1] as TestElement, 'click') await nextTick() await new Promise(r => setTimeout(r, 0)) expect(serializeInner(root)).toBe(``) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(1) // should not mount when toggling triggerEvent(root.children[1] as TestElement, 'click') await nextTick() expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(2) expect(deactiveSpy).toHaveBeenCalledTimes(1) }) test('reload class component', async () => { const root = nodeOps.createElement('div') const childId = 'test4-child' const unmountSpy = vi.fn() const mountSpy = vi.fn() class Child { static __vccOpts: ComponentOptions = { __hmrId: childId, data() { return { count: 0 } }, unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`), } } createRecord(childId, Child) const Parent: ComponentOptions = { render: () => h(Child), } render(h(Parent), root) expect(serializeInner(root)).toBe(`
0
`) class UpdatedChild { static __vccOpts: ComponentOptions = { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, render: compileToFunction(`
{{ count }}
`), } } reload(childId, UpdatedChild) await nextTick() expect(serializeInner(root)).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) }) // #6930 test('reload: avoid infinite recursion', async () => { const root = nodeOps.createElement('div') const childId = 'test-child-6930' const unmountSpy = vi.fn() const mountSpy = vi.fn() const Child: ComponentOptions = { __hmrId: childId, data() { return { count: 0 } }, expose: ['count'], unmounted: unmountSpy, render: compileToFunction(`
{{ count }}
`), } createRecord(childId, Child) const Parent: ComponentOptions = { setup() { const com1 = ref() const changeRef1 = (value: any) => (com1.value = value) const com2 = ref() const changeRef2 = (value: any) => (com2.value = value) return () => [ h(Child, { ref: changeRef1 }), h(Child, { ref: changeRef2 }), com1.value?.count, ] }, } render(h(Parent), root) await nextTick() expect(serializeInner(root)).toBe(`
0
0
0`) reload(childId, { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(serializeInner(root)).toBe(`
1
1
1`) expect(unmountSpy).toHaveBeenCalledTimes(2) expect(mountSpy).toHaveBeenCalledTimes(2) }) // #1156 - static nodes should retain DOM element reference across updates // when HMR is active test('static el reference', async () => { const root = nodeOps.createElement('div') const id = 'test-static-el' const template = `
{{ count }}
` const Comp: ComponentOptions = { __hmrId: id, data() { return { count: 0 } }, render: compileToFunction(template), } createRecord(id, Comp) render(h(Comp), root) expect(serializeInner(root)).toBe( `
0
`, ) // 1. click to trigger update triggerEvent((root as any).children[0].children[1], 'click') await nextTick() expect(serializeInner(root)).toBe( `
1
`, ) // 2. trigger HMR rerender( id, compileToFunction(template.replace(`
1
`, ) }) // #1157 - component should force full props update when HMR is active test('force update child component w/ static props', () => { const root = nodeOps.createElement('div') const parentId = 'test-force-props-parent' const childId = 'test-force-props-child' const Child: ComponentOptions = { __hmrId: childId, props: { msg: String, }, render: compileToFunction(`
{{ msg }}
`), } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(``), } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`
foo
`) rerender(parentId, compileToFunction(``)) expect(serializeInner(root)).toBe(`
bar
`) }) // #1305 - component should remove class test('remove static class from parent', () => { const root = nodeOps.createElement('div') const parentId = 'test-force-class-parent' const childId = 'test-force-class-child' const Child: ComponentOptions = { __hmrId: childId, render: compileToFunction(`
child
`), } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(``), } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`
child
`) rerender(parentId, compileToFunction(``)) expect(serializeInner(root)).toBe(`
child
`) }) test('rerender if any parent in the parent chain', () => { const root = nodeOps.createElement('div') const parent = 'test-force-props-parent-' const childId = 'test-force-props-child' const numberOfParents = 5 const Child: ComponentOptions = { __hmrId: childId, render: compileToFunction(`
child
`), } createRecord(childId, Child) const components: ComponentOptions[] = [] for (let i = 0; i < numberOfParents; i++) { const parentId = `${parent}${i}` const parentComp: ComponentOptions = { __hmrId: parentId, } components.push(parentComp) if (i === 0) { parentComp.render = compileToFunction(``) parentComp.components = { Child, } } else { parentComp.render = compileToFunction(``) parentComp.components = { Parent: components[i - 1], } } createRecord(parentId, parentComp) } const last = components[components.length - 1] render(h(last), root) expect(serializeInner(root)).toBe(`
child
`) rerender(last.__hmrId!, compileToFunction(``)) expect(serializeInner(root)).toBe(`
child
`) }) // #3302 test('rerender with Teleport', () => { const root = nodeOps.createElement('div') const target = nodeOps.createElement('div') const parentId = 'parent-teleport' const Child: ComponentOptions = { data() { return { // style is used to ensure that the div tag will be tracked by Teleport style: {}, target, } }, render: compileToFunction(`
`), } const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(` `), } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe( ``, ) expect(serializeInner(target)).toBe(`
1
`) rerender( parentId, compileToFunction(` `), ) expect(serializeInner(root)).toBe( ``, ) expect(serializeInner(target)).toBe( `
1
2
`, ) }) // #4174 test('with global mixins', async () => { const childId = 'hmr-global-mixin' const createSpy1 = vi.fn() const createSpy2 = vi.fn() const Child: ComponentOptions = { __hmrId: childId, created: createSpy1, render() { return h('div') }, } createRecord(childId, Child) const Parent: ComponentOptions = { render: () => h(Child), } const app = createApp(Parent) app.mixin({}) const root = nodeOps.createElement('div') app.mount(root) expect(createSpy1).toHaveBeenCalledTimes(1) expect(createSpy2).toHaveBeenCalledTimes(0) reload(childId, { __hmrId: childId, created: createSpy2, render() { return h('div') }, }) await nextTick() expect(createSpy1).toHaveBeenCalledTimes(1) expect(createSpy2).toHaveBeenCalledTimes(1) }) // #4757 test('rerender for component that has no active instance yet', () => { const id = 'no-active-instance-rerender' const Foo: ComponentOptions = { __hmrId: id, render: () => 'foo', } createRecord(id, Foo) rerender(id, () => 'bar') const root = nodeOps.createElement('div') render(h(Foo), root) expect(serializeInner(root)).toBe('bar') }) test('reload for component that has no active instance yet', () => { const id = 'no-active-instance-reload' const Foo: ComponentOptions = { __hmrId: id, render: () => 'foo', } createRecord(id, Foo) reload(id, { __hmrId: id, render: () => 'bar', }) const root = nodeOps.createElement('div') render(h(Foo), root) expect(serializeInner(root)).toBe('bar') }) // #7155 - force HMR on slots content update test('force update slot content change', () => { const root = nodeOps.createElement('div') const parentId = 'test-force-computed-parent' const childId = 'test-force-computed-child' const Child: ComponentOptions = { __hmrId: childId, computed: { slotContent() { return this.$slots.default?.() }, }, render: compileToFunction(``), } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(`1`), } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`1`) rerender(parentId, compileToFunction(`2`)) expect(serializeInner(root)).toBe(`2`) }) // #6978, #7138, #7114 test('hoisted children array inside v-for', () => { const root = nodeOps.createElement('div') const appId = 'test-app-id' const App: ComponentOptions = { __hmrId: appId, render: compileToFunction( `
1

2

3

`, ), } createRecord(appId, App) render(h(App), root) expect(serializeInner(root)).toBe( `
1
1

2

3

`, ) // move the

3

into the
1
rerender( appId, compileToFunction( `
1

3

2

`, ), ) expect(serializeInner(root)).toBe( `
1

3

1

3

2

`, ) }) // #11248 test('reload async component with multiple instances', async () => { const root = nodeOps.createElement('div') const childId = 'test-child-id' const Child: ComponentOptions = { __hmrId: childId, data() { return { count: 0 } }, render: compileToFunction(`
{{ count }}
`), } const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child)) const appId = 'test-app-id' const App: ComponentOptions = { __hmrId: appId, render: () => [h(Comp), h(Comp)], } createRecord(appId, App) render(h(App), root) await timeout() expect(serializeInner(root)).toBe(`
0
0
`) // change count to 1 reload(childId, { __hmrId: childId, data() { return { count: 1 } }, render: compileToFunction(`
{{ count }}
`), }) await timeout() expect(serializeInner(root)).toBe(`
1
1
`) }) test('reload async child wrapped in Suspense + KeepAlive', async () => { const id = 'async-child-reload' const AsyncChild: ComponentOptions = { __hmrId: id, async setup() { await nextTick() return () => 'foo' }, } createRecord(id, AsyncChild) const appId = 'test-app-id' const App: ComponentOptions = { __hmrId: appId, components: { AsyncChild }, render: compileToFunction(`
`), } const root = nodeOps.createElement('div') render(h(App), root) expect(serializeInner(root)).toBe('
') await timeout() expect(serializeInner(root)).toBe('
foo
') reload(id, { __hmrId: id, async setup() { await nextTick() return () => 'bar' }, }) await timeout() expect(serializeInner(root)).toBe('
bar
') }) test('multi reload child wrapped in Suspense + KeepAlive', async () => { const id = 'test-child-reload-3' const Child: ComponentOptions = { __hmrId: id, setup() { const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), } createRecord(id, Child) const appId = 'test-app-id' const App: ComponentOptions = { __hmrId: appId, components: { Child }, render: compileToFunction(` `), } const root = nodeOps.createElement('div') render(h(App), root) expect(serializeInner(root)).toBe('
0
') await timeout() reload(id, { __hmrId: id, setup() { const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await timeout() expect(serializeInner(root)).toBe('
1
') reload(id, { __hmrId: id, setup() { const count = ref(2) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await timeout() expect(serializeInner(root)).toBe('
2
') }) test('rerender for nested component', () => { const id = 'child-nested-rerender' const Foo: ComponentOptions = { __hmrId: id, render() { return this.$slots.default() }, } createRecord(id, Foo) const parentId = 'parent-nested-rerender' const Parent: ComponentOptions = { __hmrId: parentId, render() { return h(Foo, null, { default: () => this.$slots.default(), _: 3 /* FORWARDED */, }) }, } const appId = 'app-nested-rerender' const App: ComponentOptions = { __hmrId: appId, render: () => h(Parent, null, { default: () => [ h(Foo, null, { default: () => ['foo'], }), ], }), } createRecord(parentId, App) const root = nodeOps.createElement('div') render(h(App), root) expect(serializeInner(root)).toBe('foo') rerender(id, () => 'bar') expect(serializeInner(root)).toBe('bar') }) // https://github.com/vitejs/vite-plugin-vue/issues/599 // Both Outer and Inner are reloaded when './server.js' changes test('reload nested components from single update', async () => { const innerId = 'nested-reload-inner' const outerId = 'nested-reload-outer' let Inner = { __hmrId: innerId, render() { return h('div', 'foo') }, } let Outer = { __hmrId: outerId, render() { return h(Inner) }, } createRecord(innerId, Inner) createRecord(outerId, Outer) const App = { render: () => h(Outer), } const root = nodeOps.createElement('div') render(h(App), root) expect(serializeInner(root)).toBe('
foo
') Inner = { __hmrId: innerId, render() { return h('div', 'bar') }, } Outer = { __hmrId: outerId, render() { return h(Inner) }, } // trigger reload for both Outer and Inner reload(outerId, Outer) reload(innerId, Inner) await nextTick() expect(serializeInner(root)).toBe('
bar
') }) // #14127 test('update cached text nodes', async () => { const root = nodeOps.createElement('div') const appId = 'test-cached-text-nodes' const App: ComponentOptions = { __hmrId: appId, data() { return { count: 0, } }, render: compileToFunction( `{{count}} static text`, ), } createRecord(appId, App) render(h(App), root) expect(serializeInner(root)).toBe(`0 static text`) // trigger count update triggerEvent((root as any).children[2], 'click') await nextTick() expect(serializeInner(root)).toBe(`1 static text`) // trigger HMR update rerender( appId, compileToFunction( `{{count}} static text updated`, ), ) expect(serializeInner(root)).toBe( `1 static text updated`, ) // trigger HMR update again rerender( appId, compileToFunction( `{{count}} static text updated2`, ), ) expect(serializeInner(root)).toBe( `1 static text updated2`, ) }) })