test: test hot module replacement

This commit is contained in:
Evan You 2019-12-16 17:57:34 -05:00
parent f194aa0eea
commit 8ea2101553
4 changed files with 166 additions and 4 deletions

View File

@ -0,0 +1,150 @@
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)
})
})

View File

@ -5,6 +5,12 @@ import {
} from './component' } from './component'
import { queueJob, queuePostFlushCb } from './scheduler' import { queueJob, queuePostFlushCb } from './scheduler'
export interface HMRRuntime {
createRecord: typeof createRecord
rerender: typeof rerender
reload: typeof reload
}
// Expose the HMR runtime on the global object // Expose the HMR runtime on the global object
// This makes it entirely tree-shakable without polluting the exports and makes // This makes it entirely tree-shakable without polluting the exports and makes
// it easier to be used in toolings like vue-loader // it easier to be used in toolings like vue-loader
@ -24,7 +30,7 @@ if (__BUNDLER__ && __DEV__) {
createRecord: tryWrap(createRecord), createRecord: tryWrap(createRecord),
rerender: tryWrap(rerender), rerender: tryWrap(rerender),
reload: tryWrap(reload) reload: tryWrap(reload)
} } as HMRRuntime
} }
interface HMRRecord { interface HMRRecord {

View File

@ -130,3 +130,4 @@ export {
DirectiveArguments DirectiveArguments
} from './directives' } from './directives'
export { SuspenseBoundary } from './components/Suspense' export { SuspenseBoundary } from './components/Suspense'
export { HMRRuntime } from './hmr'

View File

@ -475,6 +475,7 @@ export function createRenderer<
// HMR updated, force full diff // HMR updated, force full diff
patchFlag = 0 patchFlag = 0
optimized = false optimized = false
dynamicChildren = null
} }
if (patchFlag > 0) { if (patchFlag > 0) {
@ -593,6 +594,9 @@ export function createRenderer<
) { ) {
for (let i = 0; i < newChildren.length; i++) { for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i] const oldVNode = oldChildren[i]
if (!oldVNode) {
debugger
}
patch( patch(
oldVNode, oldVNode,
newChildren[i], newChildren[i],
@ -682,7 +686,7 @@ export function createRenderer<
? n1.anchor ? n1.anchor
: hostCreateComment(showID ? `fragment-${devFragmentID}-end` : ''))! : hostCreateComment(showID ? `fragment-${devFragmentID}-end` : ''))!
let { patchFlag } = n2 let { patchFlag, dynamicChildren } = n2
if (patchFlag > 0) { if (patchFlag > 0) {
optimized = true optimized = true
} }
@ -691,6 +695,7 @@ export function createRenderer<
// HMR updated, force full diff // HMR updated, force full diff
patchFlag = 0 patchFlag = 0
optimized = false optimized = false
dynamicChildren = null
} }
if (n1 == null) { if (n1 == null) {
@ -712,12 +717,12 @@ export function createRenderer<
optimized optimized
) )
} else { } else {
if (patchFlag & PatchFlags.STABLE_FRAGMENT && n2.dynamicChildren) { if (patchFlag & PatchFlags.STABLE_FRAGMENT && dynamicChildren != null) {
// a stable fragment (template root or <template v-for>) doesn't need to // a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren. // patch children order, but it may contain dynamicChildren.
patchBlockChildren( patchBlockChildren(
n1.dynamicChildren!, n1.dynamicChildren!,
n2.dynamicChildren, dynamicChildren,
container, container,
parentComponent, parentComponent,
parentSuspense, parentSuspense,