Repository: vuejs/vue
Pull request: fix(transition): consider async placeholder as valid child to return, fix #6256

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [ ] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**
#6256 

Changed files:
filename: src/core/vdom/helpers/get-first-component-child.js
patch: @@ -1,12 +1,13 @@
 /* @flow */
 
 import { isDef } from 'shared/util'
+import { isAsyncPlaceholder } from './is-async-placeholder'
 
 export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
   if (Array.isArray(children)) {
     for (let i = 0; i < children.length; i++) {
       const c = children[i]
-      if (isDef(c) && isDef(c.componentOptions)) {
+      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
         return c
       }
     }

filename: src/core/vdom/helpers/index.js
patch: @@ -6,3 +6,4 @@ export * from './update-listeners'
 export * from './normalize-children'
 export * from './resolve-async-component'
 export * from './get-first-component-child'
+export * from './is-async-placeholder'

filename: src/core/vdom/helpers/is-async-placeholder.js
patch: @@ -0,0 +1,5 @@
+/* @flow */
+
+export function isAsyncPlaceholder (node: VNode): boolean {
+  return node.isComment && node.asyncFactory
+}

filename: src/platforms/web/runtime/components/transition.js
patch: @@ -5,7 +5,11 @@
 
 import { warn } from 'core/util/index'
 import { camelize, extend, isPrimitive } from 'shared/util'
-import { mergeVNodeHook, getFirstComponentChild } from 'core/vdom/helpers/index'
+import {
+  mergeVNodeHook,
+  isAsyncPlaceholder,
+  getFirstComponentChild
+} from 'core/vdom/helpers/index'
 
 export const transitionProps = {
   name: String,
@@ -72,10 +76,6 @@ function isSameChild (child: VNode, oldChild: VNode): boolean {
   return oldChild.key === child.key && oldChild.tag === child.tag
 }
 
-function isAsyncPlaceholder (node: VNode): boolean {
-  return node.isComment && node.asyncFactory
-}
-
 export default {
   name: 'transition',
   props: transitionProps,

filename: test/unit/features/component/component-keep-alive.spec.js
patch: @@ -862,5 +862,89 @@ describe('Component keep-alive', () => {
         )
       }).then(done)
     })
+
+    it('async components with transition-mode out-in', done => {
+      const barResolve = jasmine.createSpy('bar resolved')
+      let next
+      const foo = (resolve) => {
+        setTimeout(() => {
+          resolve(one)
+          Vue.nextTick(next)
+        }, duration / 2)
+      }
+      const bar = (resolve) => {
+        setTimeout(() => {
+          resolve(two)
+          barResolve()
+        }, duration / 2)
+      }
+      components = {
+        foo,
+        bar
+      }
+      const vm = new Vue({
+        template: `<div>
+          <transition name="test" mode="out-in" @after-enter="afterEnter" @after-leave="afterLeave">
+            <keep-alive>
+              <component :is="view" class="test"></component>
+            </keep-alive>
+          </transition>
+        </div>`,
+        data: {
+          view: 'foo'
+        },
+        components,
+        methods: {
+          afterEnter () {
+            next()
+          },
+          afterLeave () {
+            next()
+          }
+        }
+      }).$mount(el)
+      expect(vm.$el.textContent).toBe('')
+      next = () => {
+        assertHookCalls(one, [1, 1, 1, 0, 0])
+        assertHookCalls(two, [0, 0, 0, 0, 0])
+        waitForUpdate(() => {
+          expect(vm.$el.innerHTML).toBe(
+            '<div class="test test-enter test-enter-active">one</div>'
+          )
+        }).thenWaitFor(nextFrame).then(() => {
+          expect(vm.$el.innerHTML).toBe(
+            '<div class="test test-enter-active test-enter-to">one</div>'
+          )
+        }).thenWaitFor(_next => { next = _next }).then(() => {
+          // foo afterEnter get called
+          expect(vm.$el.innerHTML).toBe('<div class="test">one</div>')
+          vm.view = 'bar'
+        }).thenWaitFor(nextFrame).then(() => {
+          assertHookCalls(one, [1, 1, 1, 1, 0])
+          assertHookCalls(two, [0, 0, 0, 0, 0])
+          expect(vm.$el.innerHTML).toBe(
+            '<div class="test test-leave-active test-leave-to">one</div><!---->'
+          )
+        }).thenWaitFor(_next => { next = _next }).then(() => {
+          // foo afterLeave get called
+          // and bar has already been resolved before afterLeave get called
+          expect(barResolve.calls.count()).toBe(1)
+          expect(vm.$el.innerHTML).toBe('<!---->')
+        }).thenWaitFor(nextFrame).then(() => {
+          expect(vm.$el.innerHTML).toBe(
+            '<div class="test test-enter test-enter-active">two</div>'
+          )
+          assertHookCalls(one, [1, 1, 1, 1, 0])
+          assertHookCalls(two, [1, 1, 1, 0, 0])
+        }).thenWaitFor(nextFrame).then(() => {
+          expect(vm.$el.innerHTML).toBe(
+            '<div class="test test-enter-active test-enter-to">two</div>'
+          )
+        }).thenWaitFor(_next => { next = _next }).then(() => {
+          // bar afterEnter get called
+          expect(vm.$el.innerHTML).toBe('<div class="test">two</div>')
+        }).then(done)
+      }
+    })
   }
 })