]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: more tests for keep-alive
authorEvan You <yyx990803@gmail.com>
Thu, 31 Oct 2019 03:32:29 +0000 (23:32 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 31 Oct 2019 03:32:29 +0000 (23:32 -0400)
packages/runtime-core/__tests__/keepAlive.spec.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/keepAlive.ts

index 54d8ce5de56c2d1a73d49ebe5ab4495d345265d0..d7af1b334a0b27117f32695c33ae15a2835a7840 100644 (file)
@@ -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()
+    })
+  })
 })
index 25d6e3adf7d0a300fc95f0c4cd4c33811fc00983..7cd7f23ded8e8ca02cd2c356418ee20a229e5486 100644 (file)
@@ -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(() => {
index 5704a0b31f8e80ddfd93445e117ede53f9fd7e27..f86b9263559cdfee4f96d38976491d98c918f1f7 100644 (file)
@@ -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)