test: more tests for keep-alive
This commit is contained in:
parent
5fcb81050a
commit
4631f5323b
@ -9,15 +9,18 @@ import {
|
||||
serializeInner,
|
||||
nextTick
|
||||
} from '@vue/runtime-test'
|
||||
import { KeepAliveProps } from '../src/keepAlive'
|
||||
|
||||
describe('keep-alive', () => {
|
||||
let one: ComponentOptions
|
||||
let two: ComponentOptions
|
||||
let views: Record<string, ComponentOptions>
|
||||
let root: TestElement
|
||||
|
||||
beforeEach(() => {
|
||||
root = nodeOps.createElement('div')
|
||||
one = {
|
||||
name: 'one',
|
||||
data: () => ({ msg: 'one' }),
|
||||
render() {
|
||||
return h('div', this.msg)
|
||||
@ -29,6 +32,7 @@ describe('keep-alive', () => {
|
||||
unmounted: jest.fn()
|
||||
}
|
||||
two = {
|
||||
name: 'two',
|
||||
data: () => ({ msg: 'two' }),
|
||||
render() {
|
||||
return h('div', this.msg)
|
||||
@ -39,6 +43,10 @@ describe('keep-alive', () => {
|
||||
deactivated: jest.fn(),
|
||||
unmounted: jest.fn()
|
||||
}
|
||||
views = {
|
||||
one,
|
||||
two
|
||||
}
|
||||
})
|
||||
|
||||
function assertHookCalls(component: any, callCounts: number[]) {
|
||||
@ -52,12 +60,12 @@ describe('keep-alive', () => {
|
||||
}
|
||||
|
||||
test('should preserve state', async () => {
|
||||
const toggle = ref(true)
|
||||
const viewRef = ref('one')
|
||||
const instanceRef = ref<any>(null)
|
||||
const App = {
|
||||
render() {
|
||||
return h(KeepAlive, null, {
|
||||
default: () => h(toggle.value ? one : two, { ref: instanceRef })
|
||||
default: () => h(views[viewRef.value], { ref: instanceRef })
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -66,22 +74,20 @@ describe('keep-alive', () => {
|
||||
instanceRef.value.msg = 'changed'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>changed</div>`)
|
||||
toggle.value = false
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>two</div>`)
|
||||
toggle.value = true
|
||||
viewRef.value = 'one'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>changed</div>`)
|
||||
})
|
||||
|
||||
test('should call correct lifecycle hooks', async () => {
|
||||
const toggle1 = ref(true)
|
||||
const toggle2 = ref(true)
|
||||
const toggle = ref(true)
|
||||
const viewRef = ref('one')
|
||||
const App = {
|
||||
render() {
|
||||
return toggle1.value
|
||||
? h(KeepAlive, () => h(toggle2.value ? one : two))
|
||||
: null
|
||||
return toggle.value ? h(KeepAlive, () => h(views[viewRef.value])) : null
|
||||
}
|
||||
}
|
||||
render(h(App), root)
|
||||
@ -91,26 +97,26 @@ describe('keep-alive', () => {
|
||||
assertHookCalls(two, [0, 0, 0, 0, 0])
|
||||
|
||||
// toggle kept-alive component
|
||||
toggle2.value = false
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>two</div>`)
|
||||
assertHookCalls(one, [1, 1, 1, 1, 0])
|
||||
assertHookCalls(two, [1, 1, 1, 0, 0])
|
||||
|
||||
toggle2.value = true
|
||||
viewRef.value = 'one'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>one</div>`)
|
||||
assertHookCalls(one, [1, 1, 2, 1, 0])
|
||||
assertHookCalls(two, [1, 1, 1, 1, 0])
|
||||
|
||||
toggle2.value = false
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>two</div>`)
|
||||
assertHookCalls(one, [1, 1, 2, 2, 0])
|
||||
assertHookCalls(two, [1, 1, 2, 1, 0])
|
||||
|
||||
// teardown keep-alive, should unmount all components including cached
|
||||
toggle1.value = false
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!---->`)
|
||||
assertHookCalls(one, [1, 1, 2, 2, 1])
|
||||
@ -230,4 +236,300 @@ describe('keep-alive', () => {
|
||||
assertHookCalls(one, [1, 1, 4, 3, 0])
|
||||
assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive
|
||||
})
|
||||
|
||||
async function assertNameMatch(props: KeepAliveProps) {
|
||||
const outerRef = ref(true)
|
||||
const viewRef = ref('one')
|
||||
const App = {
|
||||
render() {
|
||||
return outerRef.value
|
||||
? h(KeepAlive, props, () => h(views[viewRef.value]))
|
||||
: null
|
||||
}
|
||||
}
|
||||
render(h(App), root)
|
||||
|
||||
expect(serializeInner(root)).toBe(`<div>one</div>`)
|
||||
assertHookCalls(one, [1, 1, 1, 0, 0])
|
||||
assertHookCalls(two, [0, 0, 0, 0, 0])
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>two</div>`)
|
||||
assertHookCalls(one, [1, 1, 1, 1, 0])
|
||||
assertHookCalls(two, [1, 1, 0, 0, 0])
|
||||
|
||||
viewRef.value = 'one'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>one</div>`)
|
||||
assertHookCalls(one, [1, 1, 2, 1, 0])
|
||||
assertHookCalls(two, [1, 1, 0, 0, 1])
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>two</div>`)
|
||||
assertHookCalls(one, [1, 1, 2, 2, 0])
|
||||
assertHookCalls(two, [2, 2, 0, 0, 1])
|
||||
|
||||
// teardown
|
||||
outerRef.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!---->`)
|
||||
assertHookCalls(one, [1, 1, 2, 2, 1])
|
||||
assertHookCalls(two, [2, 2, 0, 0, 2])
|
||||
}
|
||||
|
||||
describe('props', () => {
|
||||
test('include (string)', async () => {
|
||||
await assertNameMatch({ include: 'one' })
|
||||
})
|
||||
|
||||
test('include (regex)', async () => {
|
||||
await assertNameMatch({ include: /^one$/ })
|
||||
})
|
||||
|
||||
test('include (array)', async () => {
|
||||
await assertNameMatch({ include: ['one'] })
|
||||
})
|
||||
|
||||
test('exclude (string)', async () => {
|
||||
await assertNameMatch({ exclude: 'two' })
|
||||
})
|
||||
|
||||
test('exclude (regex)', async () => {
|
||||
await assertNameMatch({ exclude: /^two$/ })
|
||||
})
|
||||
|
||||
test('exclude (array)', async () => {
|
||||
await assertNameMatch({ exclude: ['two'] })
|
||||
})
|
||||
|
||||
test('include + exclude', async () => {
|
||||
await assertNameMatch({ include: 'one,two', exclude: 'two' })
|
||||
})
|
||||
|
||||
test('max', async () => {
|
||||
const spyA = jest.fn()
|
||||
const spyB = jest.fn()
|
||||
const spyC = jest.fn()
|
||||
const spyAD = jest.fn()
|
||||
const spyBD = jest.fn()
|
||||
const spyCD = jest.fn()
|
||||
|
||||
function assertCount(calls: number[]) {
|
||||
expect([
|
||||
spyA.mock.calls.length,
|
||||
spyAD.mock.calls.length,
|
||||
spyB.mock.calls.length,
|
||||
spyBD.mock.calls.length,
|
||||
spyC.mock.calls.length,
|
||||
spyCD.mock.calls.length
|
||||
]).toEqual(calls)
|
||||
}
|
||||
|
||||
const viewRef = ref('a')
|
||||
const views: Record<string, ComponentOptions> = {
|
||||
a: {
|
||||
render: () => `one`,
|
||||
created: spyA,
|
||||
unmounted: spyAD
|
||||
},
|
||||
b: {
|
||||
render: () => `two`,
|
||||
created: spyB,
|
||||
unmounted: spyBD
|
||||
},
|
||||
c: {
|
||||
render: () => `three`,
|
||||
created: spyC,
|
||||
unmounted: spyCD
|
||||
}
|
||||
}
|
||||
|
||||
const App = {
|
||||
render() {
|
||||
return h(KeepAlive, { max: 2 }, () => {
|
||||
return h(views[viewRef.value])
|
||||
})
|
||||
}
|
||||
}
|
||||
render(h(App), root)
|
||||
assertCount([1, 0, 0, 0, 0, 0])
|
||||
|
||||
viewRef.value = 'b'
|
||||
await nextTick()
|
||||
assertCount([1, 0, 1, 0, 0, 0])
|
||||
|
||||
viewRef.value = 'c'
|
||||
await nextTick()
|
||||
// should prune A because max cache reached
|
||||
assertCount([1, 1, 1, 0, 1, 0])
|
||||
|
||||
viewRef.value = 'b'
|
||||
await nextTick()
|
||||
// B should be reused, and made latest
|
||||
assertCount([1, 1, 1, 0, 1, 0])
|
||||
|
||||
viewRef.value = 'a'
|
||||
await nextTick()
|
||||
// C should be pruned because B was used last so C is the oldest cached
|
||||
assertCount([2, 1, 1, 0, 1, 1])
|
||||
})
|
||||
})
|
||||
|
||||
describe('cache invalidation', () => {
|
||||
function setup() {
|
||||
const viewRef = ref('one')
|
||||
const includeRef = ref('one,two')
|
||||
const App = {
|
||||
render() {
|
||||
return h(
|
||||
KeepAlive,
|
||||
{
|
||||
include: includeRef.value
|
||||
},
|
||||
() => h(views[viewRef.value])
|
||||
)
|
||||
}
|
||||
}
|
||||
render(h(App), root)
|
||||
return { viewRef, includeRef }
|
||||
}
|
||||
|
||||
test('on include/exclude change', async () => {
|
||||
const { viewRef, includeRef } = setup()
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [1, 1, 1, 1, 0])
|
||||
assertHookCalls(two, [1, 1, 1, 0, 0])
|
||||
|
||||
includeRef.value = 'two'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [1, 1, 1, 1, 1])
|
||||
assertHookCalls(two, [1, 1, 1, 0, 0])
|
||||
|
||||
viewRef.value = 'one'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [2, 2, 1, 1, 1])
|
||||
assertHookCalls(two, [1, 1, 1, 1, 0])
|
||||
})
|
||||
|
||||
test('on include/exclude change + view switch', async () => {
|
||||
const { viewRef, includeRef } = setup()
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [1, 1, 1, 1, 0])
|
||||
assertHookCalls(two, [1, 1, 1, 0, 0])
|
||||
|
||||
includeRef.value = 'one'
|
||||
viewRef.value = 'one'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [1, 1, 2, 1, 0])
|
||||
// two should be pruned
|
||||
assertHookCalls(two, [1, 1, 1, 1, 1])
|
||||
})
|
||||
|
||||
test('should not prune current active instance', async () => {
|
||||
const { viewRef, includeRef } = setup()
|
||||
|
||||
includeRef.value = 'two'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [1, 1, 1, 0, 0])
|
||||
assertHookCalls(two, [0, 0, 0, 0, 0])
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
assertHookCalls(one, [1, 1, 1, 0, 1])
|
||||
assertHookCalls(two, [1, 1, 1, 0, 0])
|
||||
})
|
||||
|
||||
async function assertAnonymous(include: boolean) {
|
||||
const one = {
|
||||
name: 'one',
|
||||
created: jest.fn(),
|
||||
render: () => 'one'
|
||||
}
|
||||
|
||||
const two = {
|
||||
// anonymous
|
||||
created: jest.fn(),
|
||||
render: () => 'two'
|
||||
}
|
||||
|
||||
const views: any = { one, two }
|
||||
const viewRef = ref('one')
|
||||
|
||||
const App = {
|
||||
render() {
|
||||
return h(
|
||||
KeepAlive,
|
||||
{
|
||||
include: include ? 'one' : undefined
|
||||
},
|
||||
() => h(views[viewRef.value])
|
||||
)
|
||||
}
|
||||
}
|
||||
render(h(App), root)
|
||||
|
||||
function assert(oneCreateCount: number, twoCreateCount: number) {
|
||||
expect(one.created.mock.calls.length).toBe(oneCreateCount)
|
||||
expect(two.created.mock.calls.length).toBe(twoCreateCount)
|
||||
}
|
||||
|
||||
assert(1, 0)
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
assert(1, 1)
|
||||
|
||||
viewRef.value = 'one'
|
||||
await nextTick()
|
||||
assert(1, 1)
|
||||
|
||||
viewRef.value = 'two'
|
||||
await nextTick()
|
||||
// two should be re-created if include is specified, since it's not matched
|
||||
// otherwise it should be cached.
|
||||
assert(1, include ? 2 : 1)
|
||||
}
|
||||
|
||||
// 2.x #6938
|
||||
test('should not cache anonymous component when include is specified', async () => {
|
||||
await assertAnonymous(true)
|
||||
})
|
||||
|
||||
test('should cache anonymous components if include is not specified', async () => {
|
||||
await assertAnonymous(false)
|
||||
})
|
||||
|
||||
// 2.x #7105
|
||||
test('should not destroy active instance when pruning cache', async () => {
|
||||
const Foo = {
|
||||
render: () => 'foo',
|
||||
unmounted: jest.fn()
|
||||
}
|
||||
const includeRef = ref(['foo'])
|
||||
const App = {
|
||||
render() {
|
||||
return h(
|
||||
KeepAlive,
|
||||
{
|
||||
include: includeRef.value
|
||||
},
|
||||
() => h(Foo)
|
||||
)
|
||||
}
|
||||
}
|
||||
render(h(App), root)
|
||||
// condition: a render where a previous component is reused
|
||||
includeRef.value = ['foo', 'bar']
|
||||
await nextTick()
|
||||
includeRef.value = []
|
||||
await nextTick()
|
||||
expect(Foo.unmounted).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -901,7 +901,11 @@ export function createRenderer<
|
||||
queuePostRenderEffect(instance.m, parentSuspense)
|
||||
}
|
||||
// activated hook for keep-alive roots.
|
||||
if (instance.a !== null) {
|
||||
if (
|
||||
instance.a !== null &&
|
||||
instance.vnode.shapeFlag &
|
||||
ShapeFlags.STATEFUL_COMPONENT_SHOULD_KEEP_ALIVE
|
||||
) {
|
||||
queuePostRenderEffect(instance.a, parentSuspense)
|
||||
}
|
||||
mounted = true
|
||||
@ -1477,7 +1481,11 @@ export function createRenderer<
|
||||
queuePostRenderEffect(um, parentSuspense)
|
||||
}
|
||||
// deactivated hook
|
||||
if (da !== null && !isDeactivated) {
|
||||
if (
|
||||
da !== null &&
|
||||
!isDeactivated &&
|
||||
instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT_SHOULD_KEEP_ALIVE
|
||||
) {
|
||||
queuePostRenderEffect(da, parentSuspense)
|
||||
}
|
||||
queuePostFlushCb(() => {
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
|
||||
type MatchPattern = string | RegExp | string[] | RegExp[]
|
||||
|
||||
interface KeepAliveProps {
|
||||
export interface KeepAliveProps {
|
||||
include?: MatchPattern
|
||||
exclude?: MatchPattern
|
||||
max?: number | string
|
||||
@ -62,16 +62,22 @@ export const KeepAlive = {
|
||||
sink.activate = (vnode, container, anchor) => {
|
||||
move(vnode, container, anchor)
|
||||
queuePostRenderEffect(() => {
|
||||
vnode.component!.isDeactivated = false
|
||||
invokeHooks(vnode.component!.a!)
|
||||
const component = vnode.component!
|
||||
component.isDeactivated = false
|
||||
if (component.a !== null) {
|
||||
invokeHooks(component.a)
|
||||
}
|
||||
}, parentSuspense)
|
||||
}
|
||||
|
||||
sink.deactivate = (vnode: VNode) => {
|
||||
move(vnode, storageContainer, null)
|
||||
queuePostRenderEffect(() => {
|
||||
invokeHooks(vnode.component!.da!)
|
||||
vnode.component!.isDeactivated = true
|
||||
const component = vnode.component!
|
||||
if (component.da !== null) {
|
||||
invokeHooks(component.da)
|
||||
}
|
||||
component.isDeactivated = true
|
||||
}, parentSuspense)
|
||||
}
|
||||
|
||||
@ -94,6 +100,10 @@ export const KeepAlive = {
|
||||
const cached = cache.get(key) as VNode
|
||||
if (!current || cached.type !== current.type) {
|
||||
unmount(cached)
|
||||
} else if (current) {
|
||||
// current active instance should no longer be kept-alive.
|
||||
// we can't unmount it now but it might be later, so reset its flag now.
|
||||
current.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
|
||||
}
|
||||
cache.delete(key)
|
||||
keys.delete(key)
|
||||
|
Loading…
Reference in New Issue
Block a user