import Vue from 'vue'
import { injectStyles, waitForUpdate, nextFrame } from './helpers'

describe('Transition basic', () => {
  const { duration, buffer } = injectStyles() as {
    duration: number
    buffer: number
  }
  const explicitDuration = duration * 2

  let el
  beforeEach(() => {
    el = document.createElement('div')
    document.body.appendChild(el)
  })

  it('basic transition', done => {
    const vm = new Vue({
      template:
        '<div><transition><div v-if="ok" class="test">foo</div></transition></div>',
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-enter-active v-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('named transition', done => {
    const vm = new Vue({
      template:
        '<div><transition name="test"><div v-if="ok" class="test">foo</div></transition></div>',
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('custom transition classes', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition
              enter-class="hello"
              enter-active-class="hello-active"
              enter-to-class="hello-to"
              leave-class="bye"
              leave-to-class="bye-to"
              leave-active-class="byebye active more ">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe('test bye byebye active more')
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test byebye active more bye-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test hello hello-active')
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test hello-active hello-to')
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('dynamic transition', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition :name="trans">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: {
        ok: true,
        trans: 'test'
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
        vm.trans = 'changed'
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test changed-enter changed-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test changed-enter-active changed-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('inline transition object', done => {
    const enter = jasmine.createSpy()
    const leave = jasmine.createSpy()
    const vm = new Vue({
      render(h) {
        return h('div', null, [
          h(
            'transition',
            {
              props: {
                name: 'inline',
                enterClass: 'hello',
                enterToClass: 'hello-to',
                enterActiveClass: 'hello-active',
                leaveClass: 'bye',
                leaveToClass: 'bye-to',
                leaveActiveClass: 'byebye active'
              },
              on: {
                enter,
                leave
              }
            },
            this.ok ? [h('div', { class: 'test' }, 'foo')] : undefined
          )
        ])
      },
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe('test bye byebye active')
      expect(leave).toHaveBeenCalled()
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test byebye active bye-to')
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test hello hello-active')
        expect(enter).toHaveBeenCalled()
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test hello-active hello-to')
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition events', done => {
    const onLeaveSpy = jasmine.createSpy()
    const onEnterSpy = jasmine.createSpy()
    const beforeLeaveSpy = jasmine.createSpy()
    const beforeEnterSpy = jasmine.createSpy()
    const afterLeaveSpy = jasmine.createSpy()
    const afterEnterSpy = jasmine.createSpy()

    const vm = new Vue({
      template: `
          <div>
            <transition
              name="test"
              @before-enter="beforeEnter"
              @enter="enter"
              @after-enter="afterEnter"
              @before-leave="beforeLeave"
              @leave="leave"
              @after-leave="afterLeave">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true },
      methods: {
        beforeLeave: el => {
          expect(el).toBe(vm.$el.children[0])
          expect(el.className).toBe('test')
          beforeLeaveSpy(el)
        },
        leave: el => onLeaveSpy(el),
        afterLeave: el => afterLeaveSpy(el),
        beforeEnter: el => {
          expect(vm.$el.contains(el)).toBe(false)
          expect(el.className).toBe('test')
          beforeEnterSpy(el)
        },
        enter: el => {
          expect(vm.$el.contains(el)).toBe(true)
          onEnterSpy(el)
        },
        afterEnter: el => afterEnterSpy(el)
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')

    let _el = vm.$el.children[0]
    vm.ok = false
    waitForUpdate(() => {
      expect(beforeLeaveSpy).toHaveBeenCalledWith(_el)
      expect(onLeaveSpy).toHaveBeenCalledWith(_el)
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(afterLeaveSpy).not.toHaveBeenCalled()
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(afterLeaveSpy).toHaveBeenCalledWith(_el)
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        _el = vm.$el.children[0]
        expect(beforeEnterSpy).toHaveBeenCalledWith(_el)
        expect(onEnterSpy).toHaveBeenCalledWith(_el)
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(afterEnterSpy).not.toHaveBeenCalled()
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(afterEnterSpy).toHaveBeenCalledWith(_el)
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition events (v-show)', done => {
    const onLeaveSpy = jasmine.createSpy()
    const onEnterSpy = jasmine.createSpy()
    const beforeLeaveSpy = jasmine.createSpy()
    const beforeEnterSpy = jasmine.createSpy()
    const afterLeaveSpy = jasmine.createSpy()
    const afterEnterSpy = jasmine.createSpy()

    const vm = new Vue({
      template: `
          <div>
            <transition
              name="test"
              @before-enter="beforeEnter"
              @enter="enter"
              @after-enter="afterEnter"
              @before-leave="beforeLeave"
              @leave="leave"
              @after-leave="afterLeave">
              <div v-show="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true },
      methods: {
        beforeLeave: el => {
          expect(el.style.display).toBe('')
          expect(el).toBe(vm.$el.children[0])
          expect(el.className).toBe('test')
          beforeLeaveSpy(el)
        },
        leave: el => {
          expect(el.style.display).toBe('')
          onLeaveSpy(el)
        },
        afterLeave: el => {
          expect(el.style.display).toBe('none')
          afterLeaveSpy(el)
        },
        beforeEnter: el => {
          expect(el.className).toBe('test')
          expect(el.style.display).toBe('none')
          beforeEnterSpy(el)
        },
        enter: el => {
          expect(el.style.display).toBe('')
          onEnterSpy(el)
        },
        afterEnter: el => {
          expect(el.style.display).toBe('')
          afterEnterSpy(el)
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')

    let _el = vm.$el.children[0]
    vm.ok = false
    waitForUpdate(() => {
      expect(beforeLeaveSpy).toHaveBeenCalledWith(_el)
      expect(onLeaveSpy).toHaveBeenCalledWith(_el)
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(afterLeaveSpy).not.toHaveBeenCalled()
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(afterLeaveSpy).toHaveBeenCalledWith(_el)
        expect(vm.$el.children[0].style.display).toBe('none')
        vm.ok = true
      })
      .then(() => {
        _el = vm.$el.children[0]
        expect(beforeEnterSpy).toHaveBeenCalledWith(_el)
        expect(onEnterSpy).toHaveBeenCalledWith(_el)
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(afterEnterSpy).not.toHaveBeenCalled()
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(afterEnterSpy).toHaveBeenCalledWith(_el)
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('explicit user callback in JavaScript hooks', done => {
    let next
    const vm = new Vue({
      template: `<div>
          <transition name="test" @enter="enter" @leave="leave">
            <div v-if="ok" class="test">foo</div>
          </transition>
        </div>`,
      data: { ok: true },
      methods: {
        enter: (el, cb) => {
          next = cb
        },
        leave: (el, cb) => {
          next = cb
        }
      }
    }).$mount(el)
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
        expect(next).toBeTruthy()
        next()
        expect(vm.$el.children.length).toBe(0)
      })
      .then(() => {
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
        expect(next).toBeTruthy()
        next()
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('css: false', done => {
    const enterSpy = jasmine.createSpy()
    const leaveSpy = jasmine.createSpy()
    const vm = new Vue({
      template: `
          <div>
            <transition :css="false" name="test" @enter="enter" @leave="leave">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true },
      methods: {
        enter: enterSpy,
        leave: leaveSpy
      }
    }).$mount(el)

    vm.ok = false
    waitForUpdate(() => {
      expect(leaveSpy).toHaveBeenCalled()
      expect(vm.$el.innerHTML).toBe('<!---->')
      vm.ok = true
    })
      .then(() => {
        expect(enterSpy).toHaveBeenCalled()
        expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
      })
      .then(done)
  })

  it('no transition detected', done => {
    const enterSpy = jasmine.createSpy()
    const leaveSpy = jasmine.createSpy()
    const vm = new Vue({
      template:
        '<div><transition name="nope" @enter="enter" @leave="leave"><div v-if="ok">foo</div></transition></div>',
      data: { ok: true },
      methods: {
        enter: enterSpy,
        leave: leaveSpy
      }
    }).$mount(el)

    vm.ok = false
    waitForUpdate(() => {
      expect(leaveSpy).toHaveBeenCalled()
      expect(vm.$el.innerHTML).toBe(
        '<div class="nope-leave nope-leave-active">foo</div><!---->'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.innerHTML).toBe('<!---->')
        vm.ok = true
      })
      .then(() => {
        expect(enterSpy).toHaveBeenCalled()
        expect(vm.$el.innerHTML).toBe(
          '<div class="nope-enter nope-enter-active">foo</div>'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.innerHTML).toBe('<div>foo</div>')
      })
      .then(done)
  })

  it('enterCancelled', done => {
    const spy = jasmine.createSpy()
    const vm = new Vue({
      template: `
          <div>
            <transition name="test" @enter-cancelled="enterCancelled">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: false },
      methods: {
        enterCancelled: spy
      }
    }).$mount(el)

    expect(vm.$el.innerHTML).toBe('<!---->')
    vm.ok = true
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-enter test-enter-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration / 2)
      .then(() => {
        vm.ok = false
      })
      .then(() => {
        expect(spy).toHaveBeenCalled()
        expect(vm.$el.children[0].className).toBe(
          'test test-leave test-leave-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
      })
      .then(done)
  })

  it('should remove stale leaving elements', done => {
    const spy = jasmine.createSpy()
    const vm = new Vue({
      template: `
          <div>
            <transition name="test" @after-leave="afterLeave">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true },
      methods: {
        afterLeave: spy
      }
    }).$mount(el)

    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(duration / 2)
      .then(() => {
        vm.ok = true
      })
      .then(() => {
        expect(spy).toHaveBeenCalled()
        expect(vm.$el.children.length).toBe(1) // should have removed leaving element
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
      })
      .then(done)
  })

  it('transition with v-show', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test">
              <div v-show="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.textContent).toBe('foo')
    expect(vm.$el.children[0].style.display).toBe('')
    expect(vm.$el.children[0].className).toBe('test')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].style.display).toBe('none')
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].style.display).toBe('')
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition with v-show, inside child component', done => {
    const vm = new Vue({
      template: `
          <div>
            <test v-show="ok"></test>
          </div>
        `,
      data: { ok: true },
      components: {
        test: {
          template: `<transition name="test"><div class="test">foo</div></transition>`
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.textContent).toBe('foo')
    expect(vm.$el.children[0].style.display).toBe('')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].style.display).toBe('none')
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].style.display).toBe('')
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('leaveCancelled (v-show only)', done => {
    const spy = jasmine.createSpy()
    const vm = new Vue({
      template: `
          <div>
            <transition name="test" @leave-cancelled="leaveCancelled">
              <div v-show="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true },
      methods: {
        leaveCancelled: spy
      }
    }).$mount(el)

    expect(vm.$el.children[0].style.display).toBe('')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(10)
      .then(() => {
        vm.ok = true
      })
      .then(() => {
        expect(spy).toHaveBeenCalled()
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].style.display).toBe('')
      })
      .then(done)
  })

  it('leave transition with v-show: cancelled on next frame', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test">
              <div v-show="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    vm.ok = false
    waitForUpdate(() => {
      vm.ok = true
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('enter transition with v-show: cancelled on next frame', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test">
              <div v-show="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: false }
    }).$mount(el)

    vm.ok = true
    waitForUpdate(() => {
      vm.ok = false
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('animations', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test-anim">
              <div v-if="ok">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div>foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test-anim-leave test-anim-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test-anim-leave-active test-anim-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test-anim-enter test-anim-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test-anim-enter-active test-anim-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('')
      })
      .then(done)
  })

  it('explicit transition type', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test-anim-long" type="animation">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-anim-long-leave test-anim-long-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-anim-long-leave-active test-anim-long-leave-to'
        )
      })
      .thenWaitFor(duration + 5)
      .then(() => {
        // should not end early due to transition presence
        expect(vm.$el.children[0].className).toBe(
          'test test-anim-long-leave-active test-anim-long-leave-to'
        )
      })
      .thenWaitFor(duration + 5)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-anim-long-enter test-anim-long-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-anim-long-enter-active test-anim-long-enter-to'
        )
      })
      .thenWaitFor(duration + 5)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-anim-long-enter-active test-anim-long-enter-to'
        )
      })
      .thenWaitFor(duration + 5)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition on appear', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test"
              appear
              appear-class="test-appear"
              appear-to-class="test-appear-to"
              appear-active-class="test-appear-active">
              <div v-if="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-appear test-appear-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-appear-active test-appear-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition on appear with v-show', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition name="test" appear>
              <div v-show="ok" class="test">foo</div>
            </transition>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-enter test-enter-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition on SVG elements', done => {
    const vm = new Vue({
      template: `
          <svg>
            <transition>
              <circle cx="0" cy="0" r="10" v-if="ok" class="test"></circle>
            </transition>
          </svg>
        `,
      data: { ok: true }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.childNodes[0].getAttribute('class')).toBe('test')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.childNodes[0].getAttribute('class')).toBe(
        'test v-leave v-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.childNodes[0].getAttribute('class')).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.childNodes.length).toBe(1)
        expect(vm.$el.childNodes[0].nodeType).toBe(8) // should be an empty comment node
        expect(vm.$el.childNodes[0].textContent).toBe('')
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.childNodes[0].getAttribute('class')).toBe(
          'test v-enter v-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.childNodes[0].getAttribute('class')).toBe(
          'test v-enter-active v-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.childNodes[0].getAttribute('class')).toBe('test')
      })
      .then(done)
  })

  it('transition on child components', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition>
              <test v-if="ok" class="test"></test>
            </transition>
          </div>
        `,
      data: { ok: true },
      components: {
        test: {
          template: `
              <transition name="test">
                <div>foo</div>
              </transition>
            ` // test transition override from parent
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test v-enter v-enter-active')
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-enter-active v-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition inside child component with v-if', done => {
    const vm = new Vue({
      template: `
          <div>
            <test v-if="ok" class="test"></test>
          </div>
        `,
      data: { ok: true },
      components: {
        test: {
          template: `
              <transition>
                <div>foo</div>
              </transition>
            `
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition with appear inside child component with v-if', done => {
    const vm = new Vue({
      template: `
          <div>
            <test v-if="ok" class="test"></test>
          </div>
        `,
      data: { ok: true },
      components: {
        test: {
          template: `
              <transition appear
                appear-class="test-appear"
                appear-to-class="test-appear-to"
                appear-active-class="test-appear-active">
                <div>foo</div>
              </transition>
            `
        }
      }
    }).$mount(el)

    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-appear test-appear-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-appear-active test-appear-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
        vm.ok = false
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
      })
      .then(done)
  })

  it('transition inside nested child component with v-if', done => {
    const vm = new Vue({
      template: `
          <div>
            <foo v-if="ok" class="test"></foo>
          </div>
        `,
      data: { ok: true },
      components: {
        foo: {
          template: '<bar></bar>',
          components: {
            bar: {
              template: '<transition><div>foo</div></transition>'
            }
          }
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('transition with appear inside nested child component with v-if', done => {
    const vm = new Vue({
      template: `
          <div>
            <foo v-if="ok" class="test"></foo>
          </div>
        `,
      data: { ok: true },
      components: {
        foo: {
          template: '<bar></bar>',
          components: {
            bar: {
              template: `
                  <transition appear
                    appear-class="test-appear"
                    appear-to-class="test-appear-to"
                    appear-active-class="test-appear-active">
                    <div>foo</div>
                  </transition>
                `
            }
          }
        }
      }
    }).$mount(el)

    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-appear test-appear-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-appear-active test-appear-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
        vm.ok = false
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
      })
      .then(done)
  })

  it('custom transition higher-order component', done => {
    const vm = new Vue({
      template:
        '<div><my-transition><div v-if="ok" class="test">foo</div></my-transition></div>',
      data: { ok: true },
      components: {
        'my-transition': {
          functional: true,
          render(h, { data, children }) {
            ;(data.props || (data.props = {})).name = 'test'
            return h('transition', data, children)
          }
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].className).toBe(
        'test test-leave test-leave-active'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-leave-active test-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children.length).toBe(0)
        vm.ok = true
      })
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter test-enter-active'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test test-enter-active test-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
      })
      .then(done)
  })

  it('warn when used on multiple elements', () => {
    new Vue({
      template: `<transition><p>1</p><p>2</p></transition>`
    }).$mount()
    expect(
      `<transition> can only be used on a single element`
    ).toHaveBeenWarned()
  })

  describe('explicit durations -', () => {
    it('single value', done => {
      const vm = new Vue({
        template: `
            <div>
              <transition duration="${explicitDuration}">
                <div v-if="ok" class="test">foo</div>
              </transition>
            </div>
          `,
        data: { ok: true }
      }).$mount(el)

      vm.ok = false

      waitForUpdate(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave-active v-leave-to'
          )
        })
        .thenWaitFor(explicitDuration + buffer)
        .then(() => {
          expect(vm.$el.children.length).toBe(0)
          vm.ok = true
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter v-enter-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter-active v-enter-to'
          )
        })
        .thenWaitFor(explicitDuration + buffer)
        .then(() => {
          expect(vm.$el.children[0].className).toBe('test')
        })
        .then(done)
    })

    it('enter and auto leave', done => {
      const vm = new Vue({
        template: `
            <div>
              <transition :duration="{ enter: ${explicitDuration} }">
                <div v-if="ok" class="test">foo</div>
              </transition>
            </div>
          `,
        data: { ok: true }
      }).$mount(el)

      vm.ok = false

      waitForUpdate(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave-active v-leave-to'
          )
        })
        .thenWaitFor(duration + buffer)
        .then(() => {
          expect(vm.$el.children.length).toBe(0)
          vm.ok = true
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter v-enter-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter-active v-enter-to'
          )
        })
        .thenWaitFor(explicitDuration + buffer)
        .then(() => {
          expect(vm.$el.children[0].className).toBe('test')
        })
        .then(done)
    })

    it('leave and auto enter', done => {
      const vm = new Vue({
        template: `
            <div>
              <transition :duration="{ leave: ${explicitDuration} }">
                <div v-if="ok" class="test">foo</div>
              </transition>
            </div>
          `,
        data: { ok: true }
      }).$mount(el)

      vm.ok = false

      waitForUpdate(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave-active v-leave-to'
          )
        })
        .thenWaitFor(explicitDuration + buffer)
        .then(() => {
          expect(vm.$el.children.length).toBe(0)
          vm.ok = true
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter v-enter-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter-active v-enter-to'
          )
        })
        .thenWaitFor(duration + buffer)
        .then(() => {
          expect(vm.$el.children[0].className).toBe('test')
        })
        .then(done)
    })

    it('separate enter and leave', done => {
      const enter = explicitDuration
      const leave = explicitDuration * 2

      const vm = new Vue({
        template: `
            <div>
              <transition :duration="{ enter: ${enter}, leave: ${leave} }">
                <div v-if="ok" class="test">foo</div>
              </transition>
            </div>
          `,
        data: { ok: true }
      }).$mount(el)

      vm.ok = false

      waitForUpdate(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave-active v-leave-to'
          )
        })
        .thenWaitFor(leave + buffer)
        .then(() => {
          expect(vm.$el.children.length).toBe(0)
          vm.ok = true
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter v-enter-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter-active v-enter-to'
          )
        })
        .thenWaitFor(enter + buffer)
        .then(() => {
          expect(vm.$el.children[0].className).toBe('test')
        })
        .then(done)
    })

    it('enter and leave + duration change', done => {
      const enter1 = explicitDuration * 2
      const enter2 = explicitDuration
      const leave1 = explicitDuration * 0.5
      const leave2 = explicitDuration * 3

      const vm = new Vue({
        template: `
            <div>
              <transition :duration="{ enter: enter, leave: leave }">
                <div v-if="ok" class="test">foo</div>
              </transition>
            </div>
          `,
        data: {
          ok: true,
          enter: enter1,
          leave: leave1
        }
      }).$mount(el)

      vm.ok = false

      waitForUpdate(() => {
        expect(vm.$el.children[0].className).toBe('test v-leave v-leave-active')
      })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave-active v-leave-to'
          )
        })
        .thenWaitFor(leave1 + buffer)
        .then(() => {
          expect(vm.$el.children.length).toBe(0)
          vm.ok = true
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter v-enter-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter-active v-enter-to'
          )
        })
        .thenWaitFor(enter1 + buffer)
        .then(() => {
          expect(vm.$el.children[0].className).toBe('test')
          vm.enter = enter2
          vm.leave = leave2
        })
        .then(() => {
          vm.ok = false
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave v-leave-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-leave-active v-leave-to'
          )
        })
        .thenWaitFor(leave2 + buffer)
        .then(() => {
          expect(vm.$el.children.length).toBe(0)
          vm.ok = true
        })
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter v-enter-active'
          )
        })
        .thenWaitFor(nextFrame)
        .then(() => {
          expect(vm.$el.children[0].className).toBe(
            'test v-enter-active v-enter-to'
          )
        })
        .thenWaitFor(enter2 + buffer)
        .then(() => {
          expect(vm.$el.children[0].className).toBe('test')
        })
        .then(done)
    }, 10000)

    it('warn invalid durations', done => {
      const vm = new Vue({
        template: `
            <div>
              <transition :duration="{ enter: NaN, leave: 'foo' }">
                <div v-if="ok" class="test">foo</div>
              </transition>
            </div>
          `,
        data: {
          ok: true
        }
      }).$mount(el)

      vm.ok = false
      waitForUpdate(() => {
        expect(
          `<transition> explicit leave duration is not a valid number - got "foo"`
        ).toHaveBeenWarned()
      })
        .thenWaitFor(duration + buffer)
        .then(() => {
          vm.ok = true
        })
        .then(() => {
          expect(
            `<transition> explicit enter duration is NaN`
          ).toHaveBeenWarned()
        })
        .then(done)
    })
  })

  // #6687
  it('transition on child components with empty root node', done => {
    const vm = new Vue({
      template: `
          <div>
            <transition mode="out-in">
              <component class="test" :is="view"></component>
            </transition>
          </div>
        `,
      data: { view: 'one' },
      components: {
        one: {
          template: '<div v-if="false">one</div>'
        },
        two: {
          template: '<div>two</div>'
        }
      }
    }).$mount(el)

    // should not apply transition on initial render by default
    expect(vm.$el.innerHTML).toBe('<!---->')
    vm.view = 'two'
    waitForUpdate(() => {
      expect(vm.$el.innerHTML).toBe(
        '<div class="test v-enter v-enter-active">two</div>'
      )
    })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-enter-active v-enter-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.children[0].className).toBe('test')
        vm.view = 'one'
      })
      .then(() => {
        // incoming comment node is appended instantly because it doesn't have
        // data and therefore doesn't go through the transition module.
        expect(vm.$el.innerHTML).toBe(
          '<div class="test v-leave v-leave-active">two</div><!---->'
        )
      })
      .thenWaitFor(nextFrame)
      .then(() => {
        expect(vm.$el.children[0].className).toBe(
          'test v-leave-active v-leave-to'
        )
      })
      .thenWaitFor(duration + buffer)
      .then(() => {
        expect(vm.$el.innerHTML).toBe('<!---->')
      })
      .then(done)
  })

  // #8199
  it('should not throw error when replaced by v-html contents', done => {
    const vm = new Vue({
      template: `
          <div>
            <div v-if="ok" :class="ok">
              <transition>
                <span>a</span>
              </transition>
            </div>
            <div v-else v-html="ok"></div>
          </div>
        `,
      data: { ok: true }
    }).$mount(el)

    vm.ok = false
    waitForUpdate(() => {
      expect(vm.$el.children[0].innerHTML).toBe('false')
    }).then(done)
  })
})