test: test for directives
This commit is contained in:
parent
94e72481b8
commit
aac807bc63
@ -1,3 +1,146 @@
|
|||||||
|
import {
|
||||||
|
h,
|
||||||
|
applyDirectives,
|
||||||
|
ref,
|
||||||
|
render,
|
||||||
|
nodeOps,
|
||||||
|
DirectiveHook,
|
||||||
|
VNode,
|
||||||
|
ComponentInstance,
|
||||||
|
DirectiveBinding,
|
||||||
|
nextTick
|
||||||
|
} from '@vue/runtime-test'
|
||||||
|
import { currentInstance } from '../src/component'
|
||||||
|
|
||||||
describe('directives', () => {
|
describe('directives', () => {
|
||||||
test.todo('should work')
|
it('should work', async () => {
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
function assertBindings(binding: DirectiveBinding) {
|
||||||
|
expect(binding.value).toBe(count.value)
|
||||||
|
expect(binding.arg).toBe('foo')
|
||||||
|
expect(binding.instance).toBe(_instance && _instance.renderProxy)
|
||||||
|
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should not be inserted yet
|
||||||
|
expect(el.parentNode).toBe(null)
|
||||||
|
expect(root.children.length).toBe(0)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode).toBe(_vnode)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should be inserted now
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode).toBe(_vnode)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
// node should not have been updated yet
|
||||||
|
expect(el.children[0].text).toBe(`${count.value - 1}`)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode).toBe(_vnode)
|
||||||
|
expect(prevVNode).toBe(_prevVnode)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
// node should have been updated
|
||||||
|
expect(el.children[0].text).toBe(`${count.value}`)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode).toBe(_vnode)
|
||||||
|
expect(prevVNode).toBe(_prevVnode)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should be removed now
|
||||||
|
expect(el.parentNode).toBe(root)
|
||||||
|
expect(root.children[0]).toBe(el)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode).toBe(_vnode)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||||
|
expect(el.tag).toBe('div')
|
||||||
|
// should have been removed
|
||||||
|
expect(el.parentNode).toBe(null)
|
||||||
|
expect(root.children.length).toBe(0)
|
||||||
|
|
||||||
|
assertBindings(binding)
|
||||||
|
|
||||||
|
expect(vnode).toBe(_vnode)
|
||||||
|
expect(prevVNode).toBe(null)
|
||||||
|
}) as DirectiveHook)
|
||||||
|
|
||||||
|
let _instance: ComponentInstance | null = null
|
||||||
|
let _vnode: VNode | null = null
|
||||||
|
let _prevVnode: VNode | null = null
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
_instance = currentInstance
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
_prevVnode = _vnode
|
||||||
|
_vnode = applyDirectives(h('div', count.value), [
|
||||||
|
{
|
||||||
|
beforeMount,
|
||||||
|
mounted,
|
||||||
|
beforeUpdate,
|
||||||
|
updated,
|
||||||
|
beforeUnmount,
|
||||||
|
unmounted
|
||||||
|
},
|
||||||
|
// value
|
||||||
|
count.value,
|
||||||
|
// argument
|
||||||
|
'foo',
|
||||||
|
// modifiers
|
||||||
|
{ ok: true }
|
||||||
|
])
|
||||||
|
return _vnode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
|
||||||
|
expect(beforeMount).toHaveBeenCalled()
|
||||||
|
expect(mounted).toHaveBeenCalled()
|
||||||
|
|
||||||
|
count.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(beforeUpdate).toHaveBeenCalled()
|
||||||
|
expect(updated).toHaveBeenCalled()
|
||||||
|
|
||||||
|
render(null, root)
|
||||||
|
expect(beforeUnmount).toHaveBeenCalled()
|
||||||
|
expect(unmounted).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -360,8 +360,16 @@ describe('error handling', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should warn unhandled', () => {
|
it('should warn unhandled', () => {
|
||||||
|
// temporarily simulate non-test env
|
||||||
|
process.env.NODE_ENV = 'dev'
|
||||||
|
|
||||||
const onError = jest.spyOn(console, 'error')
|
const onError = jest.spyOn(console, 'error')
|
||||||
onError.mockImplementation(() => {})
|
onError.mockImplementation(() => {})
|
||||||
|
const groupCollpased = jest.spyOn(console, 'groupCollapsed')
|
||||||
|
groupCollpased.mockImplementation(() => {})
|
||||||
|
const log = jest.spyOn(console, 'log')
|
||||||
|
log.mockImplementation(() => {})
|
||||||
|
|
||||||
const err = new Error('foo')
|
const err = new Error('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
@ -387,7 +395,11 @@ describe('error handling', () => {
|
|||||||
`Unhandled error during execution of setup function`
|
`Unhandled error during execution of setup function`
|
||||||
).toHaveBeenWarned()
|
).toHaveBeenWarned()
|
||||||
expect(onError).toHaveBeenCalledWith(err)
|
expect(onError).toHaveBeenCalledWith(err)
|
||||||
|
|
||||||
onError.mockRestore()
|
onError.mockRestore()
|
||||||
|
groupCollpased.mockRestore()
|
||||||
|
log.mockRestore()
|
||||||
|
process.env.NODE_ENV = 'test'
|
||||||
})
|
})
|
||||||
|
|
||||||
// native event handler handling should be tested in respective renderers
|
// native event handler handling should be tested in respective renderers
|
||||||
|
@ -299,7 +299,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
const newProps = n2.props || EMPTY_OBJ
|
const newProps = n2.props || EMPTY_OBJ
|
||||||
|
|
||||||
if (newProps.vnodeBeforeUpdate != null) {
|
if (newProps.vnodeBeforeUpdate != null) {
|
||||||
invokeDirectiveHook(newProps.vnodeBeforeUpdate, parentComponent, n2)
|
invokeDirectiveHook(newProps.vnodeBeforeUpdate, parentComponent, n2, n1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchFlag) {
|
if (patchFlag) {
|
||||||
@ -390,7 +390,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
|
|
||||||
if (newProps.vnodeUpdated != null) {
|
if (newProps.vnodeUpdated != null) {
|
||||||
queuePostFlushCb(() => {
|
queuePostFlushCb(() => {
|
||||||
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2)
|
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
} from './component'
|
} from './component'
|
||||||
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
|
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
|
||||||
|
|
||||||
interface DirectiveBinding {
|
export interface DirectiveBinding {
|
||||||
instance: ComponentRenderProxy | null
|
instance: ComponentRenderProxy | null
|
||||||
value?: any
|
value?: any
|
||||||
oldValue?: any
|
oldValue?: any
|
||||||
@ -30,20 +30,20 @@ interface DirectiveBinding {
|
|||||||
modifiers?: DirectiveModifiers
|
modifiers?: DirectiveModifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirectiveHook = (
|
export type DirectiveHook = (
|
||||||
el: any,
|
el: any,
|
||||||
binding: DirectiveBinding,
|
binding: DirectiveBinding,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
prevVNode: VNode | void
|
prevVNode: VNode | null
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
interface Directive {
|
export interface Directive {
|
||||||
beforeMount: DirectiveHook
|
beforeMount?: DirectiveHook
|
||||||
mounted: DirectiveHook
|
mounted?: DirectiveHook
|
||||||
beforeUpdate: DirectiveHook
|
beforeUpdate?: DirectiveHook
|
||||||
updated: DirectiveHook
|
updated?: DirectiveHook
|
||||||
beforeUnmount: DirectiveHook
|
beforeUnmount?: DirectiveHook
|
||||||
unmounted: DirectiveHook
|
unmounted?: DirectiveHook
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirectiveModifiers = Record<string, boolean>
|
type DirectiveModifiers = Record<string, boolean>
|
||||||
@ -64,11 +64,11 @@ function applyDirective(
|
|||||||
valueCache.set(directive, valueCacheForDir)
|
valueCache.set(directive, valueCacheForDir)
|
||||||
}
|
}
|
||||||
for (const key in directive) {
|
for (const key in directive) {
|
||||||
const hook = directive[key as keyof Directive]
|
const hook = directive[key as keyof Directive] as DirectiveHook
|
||||||
const hookKey = `vnode` + key[0].toUpperCase() + key.slice(1)
|
const hookKey = `vnode` + key[0].toUpperCase() + key.slice(1)
|
||||||
const vnodeHook = (vnode: VNode, prevVNode?: VNode) => {
|
const vnodeHook = (vnode: VNode, prevVNode: VNode | null) => {
|
||||||
let oldValue
|
let oldValue
|
||||||
if (prevVNode !== void 0) {
|
if (prevVNode != null) {
|
||||||
oldValue = valueCacheForDir.get(prevVNode)
|
oldValue = valueCacheForDir.get(prevVNode)
|
||||||
valueCacheForDir.delete(prevVNode)
|
valueCacheForDir.delete(prevVNode)
|
||||||
}
|
}
|
||||||
@ -93,12 +93,13 @@ function applyDirective(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirectiveArguments = [
|
// Directive, value, argument, modifiers
|
||||||
Directive,
|
type DirectiveArguments = Array<
|
||||||
any,
|
| [Directive]
|
||||||
string | undefined,
|
| [Directive, any]
|
||||||
DirectiveModifiers | undefined
|
| [Directive, any, string]
|
||||||
][]
|
| [Directive, any, string, DirectiveModifiers]
|
||||||
|
>
|
||||||
|
|
||||||
export function applyDirectives(
|
export function applyDirectives(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
@ -109,7 +110,7 @@ export function applyDirectives(
|
|||||||
vnode = cloneVNode(vnode)
|
vnode = cloneVNode(vnode)
|
||||||
vnode.props = vnode.props != null ? extend({}, vnode.props) : {}
|
vnode.props = vnode.props != null ? extend({}, vnode.props) : {}
|
||||||
for (let i = 0; i < directives.length; i++) {
|
for (let i = 0; i < directives.length; i++) {
|
||||||
applyDirective(vnode.props, instance, ...directives[i])
|
;(applyDirective as any)(vnode.props, instance, ...directives[i])
|
||||||
}
|
}
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn(`applyDirectives can only be used inside render functions.`)
|
warn(`applyDirectives can only be used inside render functions.`)
|
||||||
@ -125,9 +126,10 @@ export function resolveDirective(name: string): Directive {
|
|||||||
export function invokeDirectiveHook(
|
export function invokeDirectiveHook(
|
||||||
hook: Function | Function[],
|
hook: Function | Function[],
|
||||||
instance: ComponentInstance | null,
|
instance: ComponentInstance | null,
|
||||||
vnode: VNode
|
vnode: VNode,
|
||||||
|
prevVNode: VNode | null = null
|
||||||
) {
|
) {
|
||||||
const args = [vnode]
|
const args = [vnode, prevVNode]
|
||||||
if (isArray(hook)) {
|
if (isArray(hook)) {
|
||||||
for (let i = 0; i < hook.length; i++) {
|
for (let i = 0; i < hook.length; i++) {
|
||||||
callWithAsyncErrorHandling(
|
callWithAsyncErrorHandling(
|
||||||
|
@ -104,7 +104,12 @@ export function handleError(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logError(err: Error, type: AllErrorTypes, contextVNode: VNode | null) {
|
function logError(err: Error, type: AllErrorTypes, contextVNode: VNode | null) {
|
||||||
if (__DEV__) {
|
// default behavior is crash in prod & test, recover in dev.
|
||||||
|
// TODO we should probably make this configurable via `createApp`
|
||||||
|
if (
|
||||||
|
__DEV__ &&
|
||||||
|
!(typeof process !== 'undefined' && process.env.NODE_ENV === 'test')
|
||||||
|
) {
|
||||||
const info = ErrorTypeStrings[type]
|
const info = ErrorTypeStrings[type]
|
||||||
if (contextVNode) {
|
if (contextVNode) {
|
||||||
pushWarningContext(contextVNode)
|
pushWarningContext(contextVNode)
|
||||||
|
@ -45,3 +45,4 @@ export { FunctionalComponent, ComponentInstance } from './component'
|
|||||||
export { RendererOptions } from './createRenderer'
|
export { RendererOptions } from './createRenderer'
|
||||||
export { Slot, Slots } from './componentSlots'
|
export { Slot, Slots } from './componentSlots'
|
||||||
export { PropType, ComponentPropsOptions } from './componentProps'
|
export { PropType, ComponentPropsOptions } from './componentProps'
|
||||||
|
export { Directive, DirectiveBinding, DirectiveHook } from './directives'
|
||||||
|
@ -186,15 +186,19 @@ export function cloneVNode(vnode: VNode): VNode {
|
|||||||
props: vnode.props,
|
props: vnode.props,
|
||||||
key: vnode.key,
|
key: vnode.key,
|
||||||
ref: vnode.ref,
|
ref: vnode.ref,
|
||||||
children: null,
|
children: vnode.children,
|
||||||
component: null,
|
target: vnode.target,
|
||||||
el: null,
|
|
||||||
anchor: null,
|
|
||||||
target: null,
|
|
||||||
shapeFlag: vnode.shapeFlag,
|
shapeFlag: vnode.shapeFlag,
|
||||||
patchFlag: vnode.patchFlag,
|
patchFlag: vnode.patchFlag,
|
||||||
dynamicProps: vnode.dynamicProps,
|
dynamicProps: vnode.dynamicProps,
|
||||||
dynamicChildren: null
|
dynamicChildren: vnode.dynamicChildren,
|
||||||
|
|
||||||
|
// these should be set to null since they should only be present on
|
||||||
|
// mounted VNodes. If they are somehow not null, this means we have
|
||||||
|
// encountered an already-mounted vnode being used again.
|
||||||
|
component: null,
|
||||||
|
el: null,
|
||||||
|
anchor: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,12 +23,12 @@ export function popWarningContext() {
|
|||||||
export function warn(msg: string, ...args: any[]) {
|
export function warn(msg: string, ...args: any[]) {
|
||||||
// TODO app level warn handler
|
// TODO app level warn handler
|
||||||
console.warn(`[Vue warn]: ${msg}`, ...args)
|
console.warn(`[Vue warn]: ${msg}`, ...args)
|
||||||
const trace = getComponentTrace()
|
// avoid spamming console during tests
|
||||||
if (!trace.length) {
|
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// avoid spamming test output
|
const trace = getComponentTrace()
|
||||||
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
|
if (!trace.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (trace.length > 1 && console.groupCollapsed) {
|
if (trace.length > 1 && console.groupCollapsed) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user