test: test hot module replacement
This commit is contained in:
parent
f194aa0eea
commit
8ea2101553
150
packages/runtime-core/__tests__/hmr.spec.ts
Normal file
150
packages/runtime-core/__tests__/hmr.spec.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
@ -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 {
|
||||||
|
@ -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'
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user