]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(transition): in-out, appear & persisted
authorEvan You <yyx990803@gmail.com>
Wed, 27 Nov 2019 22:54:41 +0000 (17:54 -0500)
committerEvan You <yyx990803@gmail.com>
Wed, 27 Nov 2019 22:54:41 +0000 (17:54 -0500)
packages/runtime-core/__tests__/components/BaseTransition.spec.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/renderer.ts

index c78f0b6d8deffd353a2a598dfffed288cc3b6595..ac945500965129794002fc46358faf13a42fc624 100644 (file)
@@ -7,7 +7,8 @@ import {
   ref,
   nextTick,
   serializeInner,
-  serialize
+  serialize,
+  VNodeProps
 } from '@vue/runtime-test'
 
 function mount(props: BaseTransitionProps, slot: () => any) {
@@ -26,7 +27,9 @@ function mockProps(extra: BaseTransitionProps = {}) {
   }
   const props: BaseTransitionProps = {
     onBeforeEnter: jest.fn(el => {
-      expect(el.parentNode).toBeNull()
+      if (!extra.persisted) {
+        expect(el.parentNode).toBeNull()
+      }
     }),
     onEnter: jest.fn((el, done) => {
       cbs.doneEnter[serialize(el)] = done
@@ -67,8 +70,8 @@ interface ToggleOptions {
   falseSerialized: string
 }
 
-async function runTestWithElements(tester: (o: ToggleOptions) => void) {
-  await tester({
+function runTestWithElements(tester: (o: ToggleOptions) => void) {
+  return tester({
     trueBranch: () => h('div'),
     falseBranch: () => h('span'),
     trueSerialized: `<div></div>`,
@@ -76,12 +79,12 @@ async function runTestWithElements(tester: (o: ToggleOptions) => void) {
   })
 }
 
-async function runTestWithComponents(tester: (o: ToggleOptions) => void) {
+function runTestWithComponents(tester: (o: ToggleOptions) => void) {
   const CompA = ({ msg }: { msg: string }) => h('div', msg)
   // test HOC
   const CompB = ({ msg }: { msg: string }) => h(CompC, { msg })
   const CompC = ({ msg }: { msg: string }) => h('span', msg)
-  await tester({
+  return tester({
     trueBranch: () => h(CompA, { msg: 'foo' }),
     falseBranch: () => h(CompB, { msg: 'bar' }),
     trueSerialized: `<div>foo</div>`,
@@ -90,6 +93,89 @@ async function runTestWithComponents(tester: (o: ToggleOptions) => void) {
 }
 
 describe('BaseTransition', () => {
+  test('appear: true', () => {
+    const { props, cbs } = mockProps({ appear: true })
+    mount(props, () => h('div'))
+    expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
+    expect(props.onEnter).toHaveBeenCalledTimes(1)
+    expect(props.onAfterEnter).not.toHaveBeenCalled()
+    cbs.doneEnter[`<div></div>`]()
+    expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+  })
+
+  describe('persisted: true', () => {
+    // this is pretty much how v-show is implemented
+    // (but using the directive API instead)
+    function mockPersistedHooks() {
+      const state = { show: true }
+      const toggle = ref(true)
+      const hooks: VNodeProps = {
+        onVnodeBeforeMount(vnode) {
+          vnode.transition!.beforeEnter(vnode.el)
+        },
+        onVnodeMounted(vnode) {
+          vnode.transition!.enter(vnode.el)
+        },
+        onVnodeUpdated(vnode, oldVnode) {
+          if (oldVnode.props!.id !== vnode.props!.id) {
+            if (vnode.props!.id) {
+              vnode.transition!.beforeEnter(vnode.el)
+              state.show = true
+              vnode.transition!.enter(vnode.el)
+            } else {
+              vnode.transition!.leave(vnode.el, () => {
+                state.show = false
+              })
+            }
+          }
+        }
+      }
+      return { state, toggle, hooks }
+    }
+
+    test('w/ appear: false', async () => {
+      const { props, cbs } = mockProps({ persisted: true })
+      const { toggle, state, hooks } = mockPersistedHooks()
+
+      mount(props, () => h('div', { id: toggle.value, ...hooks }))
+      // without appear: true, enter hooks should not be called on mount
+      expect(props.onBeforeEnter).not.toHaveBeenCalled()
+      expect(props.onEnter).not.toHaveBeenCalled()
+      expect(props.onAfterEnter).not.toHaveBeenCalled()
+
+      toggle.value = false
+      await nextTick()
+      expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
+      expect(props.onLeave).toHaveBeenCalledTimes(1)
+      expect(props.onAfterLeave).not.toHaveBeenCalled()
+      expect(state.show).toBe(true) // should still be shown
+      cbs.doneLeave[`<div id=false></div>`]()
+      expect(state.show).toBe(false) // should be hidden now
+      expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
+
+      toggle.value = true
+      await nextTick()
+      expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
+      expect(props.onEnter).toHaveBeenCalledTimes(1)
+      expect(props.onAfterEnter).not.toHaveBeenCalled()
+      expect(state.show).toBe(true) // should be shown now
+      cbs.doneEnter[`<div id=true></div>`]()
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+    })
+
+    test('w/ appear: true', () => {
+      const { props, cbs } = mockProps({ persisted: true, appear: true })
+      const { hooks } = mockPersistedHooks()
+      mount(props, () => h('div', hooks))
+
+      expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
+      expect(props.onEnter).toHaveBeenCalledTimes(1)
+      expect(props.onAfterEnter).not.toHaveBeenCalled()
+      cbs.doneEnter[`<div></div>`]()
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+    })
+  })
+
   describe('toggle on-off', () => {
     async function testToggleOnOff({
       trueBranch,
@@ -694,15 +780,181 @@ describe('BaseTransition', () => {
     })
   })
 
-  describe('mode: "in-out"', () => {})
+  describe('mode: "in-out"', () => {
+    async function testInOut({
+      trueBranch,
+      falseBranch,
+      trueSerialized,
+      falseSerialized
+    }: ToggleOptions) {
+      const toggle = ref(true)
+      const { props, cbs } = mockProps({ mode: 'in-out' })
+      const root = mount(
+        props,
+        () => (toggle.value ? trueBranch() : falseBranch())
+      )
+
+      toggle.value = false
+      await nextTick()
+      expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
+      // enter should start
+      expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onBeforeEnter, falseSerialized)
+      expect(props.onEnter).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onEnter, falseSerialized)
+      expect(props.onAfterEnter).not.toHaveBeenCalled()
+      // leave should not start
+      expect(props.onBeforeLeave).not.toHaveBeenCalled()
+      expect(props.onLeave).not.toHaveBeenCalled()
+      expect(props.onAfterLeave).not.toHaveBeenCalled()
+
+      // finish enter
+      cbs.doneEnter[falseSerialized]()
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onAfterEnter, falseSerialized)
+
+      // leave should start now
+      expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onBeforeLeave, trueSerialized)
+      expect(props.onLeave).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onLeave, trueSerialized)
+      expect(props.onAfterLeave).not.toHaveBeenCalled()
+      // finish leave
+      cbs.doneLeave[trueSerialized]()
+      expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onAfterLeave, trueSerialized)
+
+      // toggle again
+      toggle.value = true
+      await nextTick()
+      expect(serializeInner(root)).toBe(`${falseSerialized}${trueSerialized}`)
+      // enter should start
+      expect(props.onBeforeEnter).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onBeforeEnter, trueSerialized, 1)
+      expect(props.onEnter).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onEnter, trueSerialized, 1)
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+      // leave should not start
+      expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
+      expect(props.onLeave).toHaveBeenCalledTimes(1)
+      expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
+
+      // finish enter
+      cbs.doneEnter[trueSerialized]()
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onAfterEnter, trueSerialized, 1)
+
+      // leave should start now
+      expect(props.onBeforeLeave).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onBeforeLeave, falseSerialized, 1)
+      expect(props.onLeave).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onLeave, falseSerialized, 1)
+      expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
+      // finish leave
+      cbs.doneLeave[falseSerialized]()
+      expect(props.onAfterLeave).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onAfterLeave, falseSerialized, 1)
+
+      assertCalls(props, {
+        onBeforeEnter: 2,
+        onEnter: 2,
+        onAfterEnter: 2,
+        onEnterCancelled: 0,
+        onBeforeLeave: 2,
+        onLeave: 2,
+        onAfterLeave: 2,
+        onLeaveCancelled: 0
+      })
+    }
 
-  describe('mode: "in-out" toggle before finish', () => {})
+    test('w/ elements', async () => {
+      await runTestWithElements(testInOut)
+    })
 
-  test('persisted: true', () => {
-    // test onLeaveCancelled
+    test('w/ components', async () => {
+      await runTestWithComponents(testInOut)
+    })
   })
 
-  test('appear: true', () => {})
+  describe('mode: "in-out" toggle before finish', () => {
+    async function testInOutBeforeFinish({
+      trueBranch,
+      falseBranch,
+      trueSerialized,
+      falseSerialized
+    }: ToggleOptions) {
+      const toggle = ref(true)
+      const { props, cbs } = mockProps({ mode: 'in-out' })
+      const root = mount(
+        props,
+        () => (toggle.value ? trueBranch() : falseBranch())
+      )
+
+      toggle.value = false
+      await nextTick()
+      expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
+
+      // toggle back before enter finishes
+      toggle.value = true
+      await nextTick()
+      // should force remove stale true branch
+      expect(serializeInner(root)).toBe(`${falseSerialized}${trueSerialized}`)
+      expect(props.onBeforeEnter).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onBeforeEnter, falseSerialized)
+      assertCalledWithEl(props.onBeforeEnter, trueSerialized, 1)
+      expect(props.onEnter).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onEnter, falseSerialized)
+      assertCalledWithEl(props.onEnter, trueSerialized, 1)
+      expect(props.onAfterEnter).not.toHaveBeenCalled()
+      expect(props.onEnterCancelled).not.toHaveBeenCalled()
+
+      // calling the enter done for false branch does fire the afterEnter
+      // hook, but should have no other effects since stale branch has already
+      // left
+      cbs.doneEnter[falseSerialized]()
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onAfterEnter, falseSerialized)
+
+      // leave should not start for either branch
+      expect(props.onBeforeLeave).not.toHaveBeenCalled()
+      expect(props.onLeave).not.toHaveBeenCalled()
+      expect(props.onAfterLeave).not.toHaveBeenCalled()
+
+      cbs.doneEnter[trueSerialized]()
+      expect(props.onAfterEnter).toHaveBeenCalledTimes(2)
+      assertCalledWithEl(props.onAfterEnter, trueSerialized, 1)
+      // should start leave for false branch
+      expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onBeforeLeave, falseSerialized)
+      expect(props.onLeave).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onLeave, falseSerialized)
+      expect(props.onAfterLeave).not.toHaveBeenCalled()
+      // finish leave
+      cbs.doneLeave[falseSerialized]()
+      expect(serializeInner(root)).toBe(trueSerialized)
+      expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
+      assertCalledWithEl(props.onAfterLeave, falseSerialized)
+
+      assertCalls(props, {
+        onBeforeEnter: 2,
+        onEnter: 2,
+        onAfterEnter: 2,
+        onEnterCancelled: 0,
+        onBeforeLeave: 1,
+        onLeave: 1,
+        onAfterLeave: 1,
+        onLeaveCancelled: 0
+      })
+    }
+
+    test('w/ elements', async () => {
+      await runTestWithElements(testInOutBeforeFinish)
+    })
+
+    test('w/ components', async () => {
+      await runTestWithComponents(testInOutBeforeFinish)
+    })
+  })
 
   describe('with KeepAlive', () => {
     // TODO
index 46739ded4028f415944090358f7e2777516039ef..0513cdb1a144084d5e4986387f9e9d67f4025b07 100644 (file)
@@ -48,7 +48,11 @@ export interface TransitionHooks {
   enter(el: object): void
   leave(el: object, remove: () => void): void
   afterLeave?(): void
-  delayLeave?(delayedLeave: () => void): void
+  delayLeave?(
+    el: object,
+    earlyRemove: () => void,
+    delayedLeave: () => void
+  ): void
   delayedLeave?(): void
 }
 
@@ -174,7 +178,22 @@ const BaseTransitionImpl = {
           return emptyPlaceholder(child)
         } else if (mode === 'in-out') {
           delete prevHooks.delayedLeave
-          leavingHooks.delayLeave = delayedLeave => {
+          leavingHooks.delayLeave = (
+            el: TransitionElement,
+            earlyRemove,
+            delayedLeave
+          ) => {
+            const leavingVNodesCache = getLeavingNodesForType(
+              state,
+              oldInnerChild
+            )
+            leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
+            // early removal callback
+            el._leaveCb = () => {
+              earlyRemove()
+              el._leaveCb = undefined
+              delete enterHooks.delayedLeave
+            }
             enterHooks.delayedLeave = delayedLeave
           }
         }
@@ -211,6 +230,19 @@ export const BaseTransition = (BaseTransitionImpl as any) as {
   }
 }
 
+function getLeavingNodesForType(
+  state: TransitionState,
+  vnode: VNode
+): Record<string, VNode> {
+  const { leavingVNodes } = state
+  let leavingVNodesCache = leavingVNodes.get(vnode.type)!
+  if (!leavingVNodesCache) {
+    leavingVNodesCache = Object.create(null)
+    leavingVNodes.set(vnode.type, leavingVNodesCache)
+  }
+  return leavingVNodesCache
+}
+
 // The transition hooks are attached to the vnode as vnode.transition
 // and will be called at appropriate timing in the renderer.
 function resolveTransitionHooks(
@@ -231,12 +263,7 @@ function resolveTransitionHooks(
   callHook: TransitionHookCaller
 ): TransitionHooks {
   const key = String(vnode.key)
-  const { leavingVNodes } = state
-  let leavingVNodesCache = leavingVNodes.get(vnode.type)!
-  if (!leavingVNodesCache) {
-    leavingVNodesCache = Object.create(null)
-    leavingVNodes.set(vnode.type, leavingVNodesCache)
-  }
+  const leavingVNodesCache = getLeavingNodesForType(state, vnode)
 
   const hooks: TransitionHooks = {
     persisted,
index 6a91d7e60fce14b7cc41f7573f9b561b4af756bd..63a660562dfad4779f01e41d984239f181b997da 100644 (file)
@@ -1430,14 +1430,15 @@ export function createRenderer<
           queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
         } else {
           const { leave, delayLeave, afterLeave } = transition!
+          const remove = () => hostInsert(el!, container, anchor)
           const performLeave = () => {
             leave(el!, () => {
-              hostInsert(el!, container, anchor)
+              remove()
               afterLeave && afterLeave()
             })
           }
           if (delayLeave) {
-            delayLeave(performLeave)
+            delayLeave(el!, remove, performLeave)
           } else {
             performLeave()
           }
@@ -1526,7 +1527,7 @@ export function createRenderer<
         const { leave, delayLeave } = transition
         const performLeave = () => leave(el!, remove)
         if (delayLeave) {
-          delayLeave(performLeave)
+          delayLeave(vnode.el!, remove, performLeave)
         } else {
           performLeave()
         }