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 { baseCompile } from '@vue/compiler-core' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ 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(`<div><slot/></div>`) } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, data() { return { count: 0 } }, components: { Child }, render: compileToFunction( `<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>` ) } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`<div>0<div>0</div></div>`) // 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(`<div>1<div>1</div></div>`) // // Update text while preserving state rerender( parentId, compileToFunction( `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>` ) ) expect(serializeInner(root)).toBe(`<div>1!<div>1</div></div>`) // Should force child update on slot content change rerender( parentId, compileToFunction( `<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>` ) ) expect(serializeInner(root)).toBe(`<div>1!<div>1!</div></div>`) // Should force update element children despite block optimization rerender( parentId, compileToFunction( `<div @click="count++">{{ count }}<span>{{ count }}</span> <Child>{{ count }}!</Child> </div>` ) ) expect(serializeInner(root)).toBe(`<div>1<span>1</span><div>1!</div></div>`) // Should force update child slot elements rerender( parentId, compileToFunction( `<div @click="count++"> <Child><span>{{ count }}</span></Child> </div>` ) ) expect(serializeInner(root)).toBe(`<div><div><span>1</span></div></div>`) }) 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(`<div @click="count++">{{ count }}</div>`) } createRecord(childId, Child) const Parent: ComponentOptions = { render: () => h(Child) } render(h(Parent), root) expect(serializeInner(root)).toBe(`<div>0</div>`) reload(childId, { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, render: compileToFunction(`<div @click="count++">{{ count }}</div>`) }) await nextTick() expect(serializeInner(root)).toBe(`<div>1</div>`) 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(`<div @click="count++">{{ count }}</div>`) } } createRecord(childId, Child) const Parent: ComponentOptions = { render: () => h(Child) } render(h(Parent), root) expect(serializeInner(root)).toBe(`<div>0</div>`) class UpdatedChild { static __vccOpts: ComponentOptions = { __hmrId: childId, data() { return { count: 1 } }, mounted: mountSpy, render: compileToFunction(`<div @click="count++">{{ count }}</div>`) } } reload(childId, UpdatedChild) await nextTick() expect(serializeInner(root)).toBe(`<div>1</div>`) 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 = `<div> <div>{{ count }}</div> <button @click="count++">++</button> </div>` const Comp: ComponentOptions = { __hmrId: id, data() { return { count: 0 } }, render: compileToFunction(template) } createRecord(id, Comp) render(h(Comp), root) expect(serializeInner(root)).toBe( `<div><div>0</div><button>++</button></div>` ) // 1. click to trigger update triggerEvent((root as any).children[0].children[1], 'click') await nextTick() expect(serializeInner(root)).toBe( `<div><div>1</div><button>++</button></div>` ) // 2. trigger HMR rerender( id, compileToFunction(template.replace(`<button`, `<button class="foo"`)) ) expect(serializeInner(root)).toBe( `<div><div>1</div><button class="foo">++</button></div>` ) }) // #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(`<div>{{ msg }}</div>`) } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(`<Child msg="foo" />`) } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`<div>foo</div>`) rerender(parentId, compileToFunction(`<Child msg="bar" />`)) expect(serializeInner(root)).toBe(`<div>bar</div>`) }) // #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(`<div>child</div>`) } createRecord(childId, Child) const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(`<Child class="test" />`) } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe(`<div class="test">child</div>`) rerender(parentId, compileToFunction(`<Child/>`)) expect(serializeInner(root)).toBe(`<div>child</div>`) }) 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(`<div>child</div>`) } 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(`<Child />`) parentComp.components = { Child } } else { parentComp.render = compileToFunction(`<Parent />`) parentComp.components = { Parent: components[i - 1] } } createRecord(parentId, parentComp) } const last = components[components.length - 1] render(h(last), root) expect(serializeInner(root)).toBe(`<div>child</div>`) rerender(last.__hmrId!, compileToFunction(`<Parent class="test"/>`)) expect(serializeInner(root)).toBe(`<div class="test">child</div>`) }) // #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(` <teleport :to="target"> <div :style="style"> <slot/> </div> </teleport> `) } const Parent: ComponentOptions = { __hmrId: parentId, components: { Child }, render: compileToFunction(` <Child> <template #default> <div>1</div> </template> </Child> `) } createRecord(parentId, Parent) render(h(Parent), root) expect(serializeInner(root)).toBe( `<!--teleport start--><!--teleport end-->` ) expect(serializeInner(target)).toBe(`<div style={}><div>1</div></div>`) rerender( parentId, compileToFunction(` <Child> <template #default> <div>1</div> <div>2</div> </template> </Child> `) ) expect(serializeInner(root)).toBe( `<!--teleport start--><!--teleport end-->` ) expect(serializeInner(target)).toBe( `<div style={}><div>1</div><div>2</div></div>` ) }) })