test: wip more tests for BaseTransition

This commit is contained in:
Evan You 2019-11-27 12:17:16 -05:00
parent fbcc47841b
commit 7209fb66c2
2 changed files with 355 additions and 41 deletions

View File

@ -6,7 +6,8 @@ import {
BaseTransitionProps, BaseTransitionProps,
ref, ref,
nextTick, nextTick,
serializeInner serializeInner,
serialize
} from '@vue/runtime-test' } from '@vue/runtime-test'
function mount(props: BaseTransitionProps, slot: () => any) { function mount(props: BaseTransitionProps, slot: () => any) {
@ -16,22 +17,25 @@ function mount(props: BaseTransitionProps, slot: () => any) {
} }
function mockProps() { function mockProps() {
const cbs = { const cbs: {
doneEnter: () => {}, doneEnter: Record<string, () => void>
doneLeave: () => {} doneLeave: Record<string, () => void>
} = {
doneEnter: {},
doneLeave: {}
} }
const props: BaseTransitionProps = { const props: BaseTransitionProps = {
onBeforeEnter: jest.fn(el => { onBeforeEnter: jest.fn(el => {
expect(el.parentNode).toBeNull() expect(el.parentNode).toBeNull()
}), }),
onEnter: jest.fn((el, done) => { onEnter: jest.fn((el, done) => {
cbs.doneEnter = done cbs.doneEnter[serialize(el)] = done
}), }),
onAfterEnter: jest.fn(), onAfterEnter: jest.fn(),
onEnterCancelled: jest.fn(), onEnterCancelled: jest.fn(),
onBeforeLeave: jest.fn(), onBeforeLeave: jest.fn(),
onLeave: jest.fn((el, done) => { onLeave: jest.fn((el, done) => {
cbs.doneLeave = done cbs.doneLeave[serialize(el)] = done
}), }),
onAfterLeave: jest.fn(), onAfterLeave: jest.fn(),
onLeaveCancelled: jest.fn() onLeaveCancelled: jest.fn()
@ -51,12 +55,31 @@ function assertCalls(
}) })
} }
function assertCalledWithEl(fn: any, expected: string, callIndex = 0) {
expect(serialize((fn as jest.Mock).mock.calls[callIndex][0])).toBe(expected)
}
interface ToggleOptions {
trueBranch: () => any
falseBranch: () => any
trueSerialized: string
falseSerialized: string
}
describe('BaseTransition', () => { describe('BaseTransition', () => {
describe('with elements', () => { describe('toggle on-off', () => {
test('toggle on-off', async () => { async function testToggleOnOff({
trueBranch,
trueSerialized,
falseBranch,
falseSerialized
}: ToggleOptions) {
const toggle = ref(true) const toggle = ref(true)
const { props, cbs } = mockProps() const { props, cbs } = mockProps()
const root = mount(props, () => (toggle.value ? h('div') : null)) const root = mount(
props,
() => (toggle.value ? trueBranch() : falseBranch())
)
// without appear: true, enter hooks should not be called on mount // without appear: true, enter hooks should not be called on mount
expect(props.onBeforeEnter).not.toHaveBeenCalled() expect(props.onBeforeEnter).not.toHaveBeenCalled()
@ -66,23 +89,29 @@ describe('BaseTransition', () => {
toggle.value = false toggle.value = false
await nextTick() await nextTick()
// comment placeholder enters immediately // comment placeholder enters immediately
expect(serializeInner(root)).toBe('<div></div><!---->') expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1) expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onBeforeLeave, trueSerialized)
expect(props.onLeave).toHaveBeenCalledTimes(1) expect(props.onLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onLeave, trueSerialized)
expect(props.onAfterLeave).not.toHaveBeenCalled() expect(props.onAfterLeave).not.toHaveBeenCalled()
cbs.doneLeave() cbs.doneLeave[trueSerialized]()
expect(serializeInner(root)).toBe('<!---->') expect(serializeInner(root)).toBe(falseSerialized)
expect(props.onAfterLeave).toHaveBeenCalledTimes(1) expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onAfterLeave, trueSerialized)
toggle.value = true toggle.value = true
await nextTick() await nextTick()
expect(serializeInner(root)).toBe('<div></div>') expect(serializeInner(root)).toBe(trueSerialized)
// before enter spy asserts node has no parent when it's called // before enter spy asserts node has no parent when it's called
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1) expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onBeforeEnter, trueSerialized)
expect(props.onEnter).toHaveBeenCalledTimes(1) expect(props.onEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onEnter, trueSerialized)
expect(props.onAfterEnter).not.toHaveBeenCalled() expect(props.onAfterEnter).not.toHaveBeenCalled()
cbs.doneEnter() cbs.doneEnter[trueSerialized]()
expect(props.onAfterEnter).toHaveBeenCalledTimes(1) expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onAfterEnter, trueSerialized)
assertCalls(props, { assertCalls(props, {
onBeforeEnter: 1, onBeforeEnter: 1,
@ -94,30 +123,59 @@ describe('BaseTransition', () => {
onAfterLeave: 1, onAfterLeave: 1,
onLeaveCancelled: 0 onLeaveCancelled: 0
}) })
}
test('w/ element', async () => {
await testToggleOnOff({
trueBranch: () => h('div'),
trueSerialized: `<div></div>`,
falseBranch: () => null,
falseSerialized: `<!---->`
})
}) })
test('toggle before finish', async () => { test('w/ component', async () => {
const Comp = ({ msg }: { msg: string }) => h('div', msg)
await testToggleOnOff({
trueBranch: () => h(Comp, { msg: 'hello' }),
trueSerialized: `<div>hello</div>`,
falseBranch: () => null,
falseSerialized: `<!---->`
})
})
})
describe('toggle on-off before finish', () => {
async function testToggleOnOffBeforeFinish({
trueBranch,
trueSerialized,
falseBranch = () => null,
falseSerialized = `<!---->`
}: ToggleOptions) {
const toggle = ref(false) const toggle = ref(false)
const { props, cbs } = mockProps() const { props, cbs } = mockProps()
const root = mount(props, () => (toggle.value ? h('div') : null)) const root = mount(
props,
() => (toggle.value ? trueBranch() : falseBranch())
)
// start enter // start enter
toggle.value = true toggle.value = true
await nextTick() await nextTick()
expect(serializeInner(root)).toBe(`<div></div>`) expect(serializeInner(root)).toBe(trueSerialized)
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1) expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
expect(props.onEnter).toHaveBeenCalledTimes(1) expect(props.onEnter).toHaveBeenCalledTimes(1)
// leave before enter finishes // leave before enter finishes
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(serializeInner(root)).toBe(`<div></div><!---->`) expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
expect(props.onEnterCancelled).toHaveBeenCalled() expect(props.onEnterCancelled).toHaveBeenCalled()
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1) expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
expect(props.onLeave).toHaveBeenCalledTimes(1) expect(props.onLeave).toHaveBeenCalledTimes(1)
expect(props.onAfterLeave).not.toHaveBeenCalled() expect(props.onAfterLeave).not.toHaveBeenCalled()
// calling doneEnter now should have no effect // calling doneEnter now should have no effect
cbs.doneEnter() cbs.doneEnter[trueSerialized]()
expect(props.onAfterEnter).not.toHaveBeenCalled() expect(props.onAfterEnter).not.toHaveBeenCalled()
// enter again before leave finishes // enter again before leave finishes
@ -127,14 +185,14 @@ describe('BaseTransition', () => {
expect(props.onEnter).toHaveBeenCalledTimes(2) expect(props.onEnter).toHaveBeenCalledTimes(2)
// 1. should remove the previous leaving <div> so there is only one <div> // 1. should remove the previous leaving <div> so there is only one <div>
// 2. should remove the comment placeholder for the off branch // 2. should remove the comment placeholder for the off branch
expect(serializeInner(root)).toBe(`<div></div>`) expect(serializeInner(root)).toBe(trueSerialized)
// note onLeaveCancelled is NOT called because it was a forced early // note onLeaveCancelled is NOT called because it was a forced early
// removal instead of a cancel. Instead, onAfterLeave should be called. // removal instead of a cancel. Instead, onAfterLeave should be called.
expect(props.onAfterLeave).toHaveBeenCalledTimes(1) expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
// calling doneLeave again should have no effect now // calling doneLeave again should have no effect now
cbs.doneLeave() cbs.doneLeave[trueSerialized]()
expect(props.onAfterLeave).toHaveBeenCalledTimes(1) expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
cbs.doneEnter() cbs.doneEnter[trueSerialized]()
expect(props.onAfterEnter).toHaveBeenCalledTimes(1) expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
assertCalls(props, { assertCalls(props, {
@ -147,29 +205,278 @@ describe('BaseTransition', () => {
onAfterLeave: 1, onAfterLeave: 1,
onLeaveCancelled: 0 onLeaveCancelled: 0
}) })
}
test('w/ element', async () => {
await testToggleOnOffBeforeFinish({
trueBranch: () => h('div'),
trueSerialized: `<div></div>`,
falseBranch: () => null,
falseSerialized: `<!---->`
})
}) })
test('toggle between branches', () => {}) test('w/ component', async () => {
const Comp = ({ msg }: { msg: string }) => h('div', msg)
await testToggleOnOffBeforeFinish({
trueBranch: () => h(Comp, { msg: 'hello' }),
trueSerialized: `<div>hello</div>`,
falseBranch: () => null,
falseSerialized: `<!---->`
})
})
})
test('toggle between branches before finish', () => {}) describe('toggle between branches', () => {
async function testToggleBranches({
trueBranch,
falseBranch,
trueSerialized,
falseSerialized
}: ToggleOptions) {
const toggle = ref(true)
const { props, cbs } = mockProps()
const root = mount(
props,
() => (toggle.value ? trueBranch() : falseBranch())
)
test('persisted: true', () => { // without appear: true, enter hooks should not be called on mount
// test onLeaveCancelled expect(props.onBeforeEnter).not.toHaveBeenCalled()
expect(props.onEnter).not.toHaveBeenCalled()
expect(props.onAfterEnter).not.toHaveBeenCalled()
// start toggle
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
// leave should be triggered
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onBeforeLeave, trueSerialized)
expect(props.onLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onLeave, trueSerialized)
expect(props.onAfterLeave).not.toHaveBeenCalled()
// enter should also be triggered
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onBeforeEnter, falseSerialized)
expect(props.onEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onEnter, falseSerialized)
expect(props.onAfterEnter).not.toHaveBeenCalled()
// finish enter
cbs.doneEnter[falseSerialized]()
expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onAfterEnter, falseSerialized)
expect(props.onAfterLeave).not.toHaveBeenCalled()
// finish leave
cbs.doneLeave[trueSerialized]()
expect(serializeInner(root)).toBe(`${falseSerialized}`)
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onAfterLeave, trueSerialized)
// toggle again
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe(`${falseSerialized}${trueSerialized}`)
// leave should be triggered
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)
// enter should also be triggered
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)
// finish leave first
cbs.doneLeave[falseSerialized]()
expect(serializeInner(root)).toBe(`${trueSerialized}`)
expect(props.onAfterLeave).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onAfterLeave, falseSerialized, 1)
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
// finish enter
cbs.doneEnter[trueSerialized]()
expect(serializeInner(root)).toBe(`${trueSerialized}`)
expect(props.onAfterEnter).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onAfterEnter, trueSerialized, 1)
assertCalls(props, {
onBeforeEnter: 2,
onEnter: 2,
onAfterEnter: 2,
onBeforeLeave: 2,
onLeave: 2,
onAfterLeave: 2,
onEnterCancelled: 0,
onLeaveCancelled: 0
})
}
test('w/ elements', async () => {
await testToggleBranches({
trueBranch: () => h('div'),
falseBranch: () => h('span'),
trueSerialized: `<div></div>`,
falseSerialized: `<span></span>`
})
}) })
test('appear: true', () => {}) test('w/ components', async () => {
const CompA = ({ msg }: { msg: string }) => h('div', msg)
test('mode: "out-in"', () => {}) // test HOC
const CompB = ({ msg }: { msg: string }) => h(CompC, { msg })
test('mode: "out-in" toggle before finish', () => {}) const CompC = ({ msg }: { msg: string }) => h('span', msg)
await testToggleBranches({
test('mode: "in-out" toggle before finish', () => {}) trueBranch: () => h(CompA, { msg: 'foo' }),
falseBranch: () => h(CompB, { msg: 'bar' }),
trueSerialized: `<div>foo</div>`,
falseSerialized: `<span>bar</span>`
})
})
}) })
describe('with components', () => { describe('toggle between branches before finish', () => {
// TODO async function testToggleBranchesBeforeFinish({
trueBranch,
falseBranch,
trueSerialized,
falseSerialized
}: ToggleOptions) {
const toggle = ref(true)
const { props, cbs } = mockProps()
const root = mount(
props,
() => (toggle.value ? trueBranch() : falseBranch())
)
// start toggle
toggle.value = false
await nextTick()
expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
// leave should be triggered
expect(props.onBeforeLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onBeforeLeave, trueSerialized)
expect(props.onLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onLeave, trueSerialized)
expect(props.onAfterLeave).not.toHaveBeenCalled()
// enter should also be triggered
expect(props.onBeforeEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onBeforeEnter, falseSerialized)
expect(props.onEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onEnter, falseSerialized)
expect(props.onAfterEnter).not.toHaveBeenCalled()
// toggle again before transition finishes
toggle.value = true
await nextTick()
// the previous leaving true branch should have been force-removed
expect(serializeInner(root)).toBe(`${falseSerialized}${trueSerialized}`)
expect(props.onAfterLeave).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onAfterLeave, trueSerialized)
// false branch enter is cancelled
expect(props.onEnterCancelled).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onEnterCancelled, falseSerialized)
// calling false branch done should have no effect now
cbs.doneEnter[falseSerialized]()
expect(props.onAfterEnter).not.toHaveBeenCalled()
// false branch leave triggered
expect(props.onBeforeLeave).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onBeforeLeave, falseSerialized, 1)
expect(props.onLeave).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onLeave, falseSerialized, 1)
// true branch enter triggered
expect(props.onBeforeEnter).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onBeforeEnter, trueSerialized, 1)
expect(props.onEnter).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onEnter, trueSerialized, 1)
expect(props.onAfterEnter).not.toHaveBeenCalled()
// toggle again
toggle.value = false
await nextTick()
// the previous leaving false branch should have been force-removed
expect(serializeInner(root)).toBe(`${trueSerialized}${falseSerialized}`)
expect(props.onAfterLeave).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onAfterLeave, falseSerialized, 1)
// true branch enter is cancelled
expect(props.onEnterCancelled).toHaveBeenCalledTimes(2)
assertCalledWithEl(props.onEnterCancelled, trueSerialized, 1)
// calling true branch enter done should have no effect
cbs.doneEnter[trueSerialized]()
expect(props.onAfterEnter).not.toHaveBeenCalled()
// true branch leave triggered (again)
expect(props.onBeforeLeave).toHaveBeenCalledTimes(3)
assertCalledWithEl(props.onBeforeLeave, trueSerialized, 2)
expect(props.onLeave).toHaveBeenCalledTimes(3)
assertCalledWithEl(props.onLeave, trueSerialized, 2)
// false branch enter triggered (again)
expect(props.onBeforeEnter).toHaveBeenCalledTimes(3)
assertCalledWithEl(props.onBeforeEnter, falseSerialized, 2)
expect(props.onEnter).toHaveBeenCalledTimes(3)
assertCalledWithEl(props.onEnter, falseSerialized, 2)
expect(props.onAfterEnter).not.toHaveBeenCalled()
cbs.doneEnter[falseSerialized]()
expect(props.onAfterEnter).toHaveBeenCalledTimes(1)
assertCalledWithEl(props.onAfterEnter, falseSerialized)
cbs.doneLeave[trueSerialized]()
expect(props.onAfterLeave).toHaveBeenCalledTimes(3)
assertCalledWithEl(props.onAfterLeave, trueSerialized, 2)
assertCalls(props, {
onBeforeEnter: 3,
onEnter: 3,
onAfterEnter: 1,
onEnterCancelled: 2,
onBeforeLeave: 3,
onLeave: 3,
onAfterLeave: 3,
onLeaveCancelled: 0
})
}
test('w/ elements', async () => {
await testToggleBranchesBeforeFinish({
trueBranch: () => h('div'),
falseBranch: () => h('span'),
trueSerialized: `<div></div>`,
falseSerialized: `<span></span>`
})
})
test('w/ components', async () => {
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 testToggleBranchesBeforeFinish({
trueBranch: () => h(CompA, { msg: 'foo' }),
falseBranch: () => h(CompB, { msg: 'bar' }),
trueSerialized: `<div>foo</div>`,
falseSerialized: `<span>bar</span>`
})
})
}) })
describe('mode: "out-in"', () => {})
describe('mode: "out-in" toggle before finish', () => {})
describe('mode: "in-out"', () => {})
describe('mode: "in-out" toggle before finish', () => {})
test('persisted: true', () => {
// test onLeaveCancelled
})
test('appear: true', () => {})
describe('with KeepAlive', () => { describe('with KeepAlive', () => {
// TODO // TODO
}) })

View File

@ -65,7 +65,7 @@ interface TransitionState {
isUnmounting: boolean isUnmounting: boolean
// Track pending leave callbacks for children of the same key. // Track pending leave callbacks for children of the same key.
// This is used to force remove leaving a child when a new copy is entering. // This is used to force remove leaving a child when a new copy is entering.
leavingVNodes: Record<string, VNode> leavingVNodes: Map<any, Record<string, VNode>>
} }
interface TransitionElement { interface TransitionElement {
@ -84,7 +84,7 @@ const BaseTransitionImpl = {
isMounted: false, isMounted: false,
isLeaving: false, isLeaving: false,
isUnmounting: false, isUnmounting: false,
leavingVNodes: Object.create(null) leavingVNodes: new Map()
} }
onMounted(() => { onMounted(() => {
state.isMounted = true state.isMounted = true
@ -230,8 +230,13 @@ function resolveTransitionHooks(
state: TransitionState, state: TransitionState,
callHook: TransitionHookCaller callHook: TransitionHookCaller
): TransitionHooks { ): TransitionHooks {
const { leavingVNodes } = state
const key = String(vnode.key) 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 hooks: TransitionHooks = { const hooks: TransitionHooks = {
persisted, persisted,
@ -244,7 +249,7 @@ function resolveTransitionHooks(
el._leaveCb(true /* cancelled */) el._leaveCb(true /* cancelled */)
} }
// for toggled element with same key (v-if) // for toggled element with same key (v-if)
const leavingVNode = leavingVNodes[key] const leavingVNode = leavingVNodesCache[key]
if ( if (
leavingVNode && leavingVNode &&
isSameVNodeType(vnode, leavingVNode) && isSameVNodeType(vnode, leavingVNode) &&
@ -301,9 +306,11 @@ function resolveTransitionHooks(
callHook(onAfterLeave, [el]) callHook(onAfterLeave, [el])
} }
el._leaveCb = undefined el._leaveCb = undefined
delete leavingVNodes[key] if (leavingVNodesCache[key] === vnode) {
delete leavingVNodesCache[key]
}
}) })
leavingVNodes[key] = vnode leavingVNodesCache[key] = vnode
if (onLeave) { if (onLeave) {
onLeave(el, afterLeave) onLeave(el, afterLeave)
} else { } else {