--- /dev/null
+import { HMRRuntime } from '../src/hmr'
+import '../src/hmr'
+import { ComponentOptions, RenderFunction } 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 RenderFunction
+ render.isRuntimeCompiled = true
+ 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(`<slot/>`)
+ }
+ 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<!---->0<!----></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<!---->1<!----></div>`)
+
+ // Update text while preserving state
+ rerender(
+ parentId,
+ compileToFunction(
+ `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
+ )
+ )
+ expect(serializeInner(root)).toBe(`<div>1!<!---->1<!----></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!<!---->1!<!----></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><!---->1!<!----></div>`
+ )
+
+ // Should force update child slot elements
+ rerender(
+ parentId,
+ compileToFunction(
+ `<div @click="count++">
+ <Child><span>{{ count }}</span></Child>
+ </div>`
+ )
+ )
+ expect(serializeInner(root)).toBe(`<div><!----><span>1</span><!----></div>`)
+ })
+
+ test('reload', async () => {
+ const root = nodeOps.createElement('div')
+ const childId = 'test3-child'
+ const unmoutSpy = jest.fn()
+ const mountSpy = jest.fn()
+
+ const Child: ComponentOptions = {
+ __hmrId: childId,
+ data() {
+ return { count: 0 }
+ },
+ unmounted: unmoutSpy,
+ 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(unmoutSpy).toHaveBeenCalledTimes(1)
+ expect(mountSpy).toHaveBeenCalledTimes(1)
+ })
+})