import { HMRRuntime } from '../src/hmr' import '../src/hmr' import { ComponentOptions, InternalRenderFunction } from '../src/component' import { render, nodeOps, h, serializeInner, triggerEvent, TestElement, nextTick } from '@vue/runtime-test' import * as runtimeTest from '@vue/runtime-test' import { registerRuntimeCompiler, createApp } 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) const render = new Function('Vue', code)( runtimeTest ) as InternalRenderFunction render._rc = true // isRuntimeCompiled return render } 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 = jest.fn() const mountSpy = jest.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) }) test('reload class component', async () => { const root = nodeOps.createElement('div') const childId = 'test4-child' const unmountSpy = jest.fn() const mountSpy = jest.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) }) // #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 = jest.fn() const createSpy2 = jest.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') }) })