test: test for directives

This commit is contained in:
Evan You 2019-09-02 12:09:29 -04:00
parent 94e72481b8
commit aac807bc63
8 changed files with 203 additions and 36 deletions

View File

@ -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', () => {
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()
})
})

View File

@ -360,8 +360,16 @@ describe('error handling', () => {
})
it('should warn unhandled', () => {
// temporarily simulate non-test env
process.env.NODE_ENV = 'dev'
const onError = jest.spyOn(console, 'error')
onError.mockImplementation(() => {})
const groupCollpased = jest.spyOn(console, 'groupCollapsed')
groupCollpased.mockImplementation(() => {})
const log = jest.spyOn(console, 'log')
log.mockImplementation(() => {})
const err = new Error('foo')
const fn = jest.fn()
@ -387,7 +395,11 @@ describe('error handling', () => {
`Unhandled error during execution of setup function`
).toHaveBeenWarned()
expect(onError).toHaveBeenCalledWith(err)
onError.mockRestore()
groupCollpased.mockRestore()
log.mockRestore()
process.env.NODE_ENV = 'test'
})
// native event handler handling should be tested in respective renderers

View File

@ -299,7 +299,7 @@ export function createRenderer(options: RendererOptions) {
const newProps = n2.props || EMPTY_OBJ
if (newProps.vnodeBeforeUpdate != null) {
invokeDirectiveHook(newProps.vnodeBeforeUpdate, parentComponent, n2)
invokeDirectiveHook(newProps.vnodeBeforeUpdate, parentComponent, n2, n1)
}
if (patchFlag) {
@ -390,7 +390,7 @@ export function createRenderer(options: RendererOptions) {
if (newProps.vnodeUpdated != null) {
queuePostFlushCb(() => {
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2)
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1)
})
}
}

View File

@ -22,7 +22,7 @@ import {
} from './component'
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
interface DirectiveBinding {
export interface DirectiveBinding {
instance: ComponentRenderProxy | null
value?: any
oldValue?: any
@ -30,20 +30,20 @@ interface DirectiveBinding {
modifiers?: DirectiveModifiers
}
type DirectiveHook = (
export type DirectiveHook = (
el: any,
binding: DirectiveBinding,
vnode: VNode,
prevVNode: VNode | void
prevVNode: VNode | null
) => void
interface Directive {
beforeMount: DirectiveHook
mounted: DirectiveHook
beforeUpdate: DirectiveHook
updated: DirectiveHook
beforeUnmount: DirectiveHook
unmounted: DirectiveHook
export interface Directive {
beforeMount?: DirectiveHook
mounted?: DirectiveHook
beforeUpdate?: DirectiveHook
updated?: DirectiveHook
beforeUnmount?: DirectiveHook
unmounted?: DirectiveHook
}
type DirectiveModifiers = Record<string, boolean>
@ -64,11 +64,11 @@ function applyDirective(
valueCache.set(directive, valueCacheForDir)
}
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 vnodeHook = (vnode: VNode, prevVNode?: VNode) => {
const vnodeHook = (vnode: VNode, prevVNode: VNode | null) => {
let oldValue
if (prevVNode !== void 0) {
if (prevVNode != null) {
oldValue = valueCacheForDir.get(prevVNode)
valueCacheForDir.delete(prevVNode)
}
@ -93,12 +93,13 @@ function applyDirective(
}
}
type DirectiveArguments = [
Directive,
any,
string | undefined,
DirectiveModifiers | undefined
][]
// Directive, value, argument, modifiers
type DirectiveArguments = Array<
| [Directive]
| [Directive, any]
| [Directive, any, string]
| [Directive, any, string, DirectiveModifiers]
>
export function applyDirectives(
vnode: VNode,
@ -109,7 +110,7 @@ export function applyDirectives(
vnode = cloneVNode(vnode)
vnode.props = vnode.props != null ? extend({}, vnode.props) : {}
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__) {
warn(`applyDirectives can only be used inside render functions.`)
@ -125,9 +126,10 @@ export function resolveDirective(name: string): Directive {
export function invokeDirectiveHook(
hook: Function | Function[],
instance: ComponentInstance | null,
vnode: VNode
vnode: VNode,
prevVNode: VNode | null = null
) {
const args = [vnode]
const args = [vnode, prevVNode]
if (isArray(hook)) {
for (let i = 0; i < hook.length; i++) {
callWithAsyncErrorHandling(

View File

@ -104,7 +104,12 @@ export function handleError(
}
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]
if (contextVNode) {
pushWarningContext(contextVNode)

View File

@ -45,3 +45,4 @@ export { FunctionalComponent, ComponentInstance } from './component'
export { RendererOptions } from './createRenderer'
export { Slot, Slots } from './componentSlots'
export { PropType, ComponentPropsOptions } from './componentProps'
export { Directive, DirectiveBinding, DirectiveHook } from './directives'

View File

@ -186,15 +186,19 @@ export function cloneVNode(vnode: VNode): VNode {
props: vnode.props,
key: vnode.key,
ref: vnode.ref,
children: null,
component: null,
el: null,
anchor: null,
target: null,
children: vnode.children,
target: vnode.target,
shapeFlag: vnode.shapeFlag,
patchFlag: vnode.patchFlag,
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
}
}

View File

@ -23,12 +23,12 @@ export function popWarningContext() {
export function warn(msg: string, ...args: any[]) {
// TODO app level warn handler
console.warn(`[Vue warn]: ${msg}`, ...args)
const trace = getComponentTrace()
if (!trace.length) {
// avoid spamming console during tests
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
return
}
// avoid spamming test output
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
const trace = getComponentTrace()
if (!trace.length) {
return
}
if (trace.length > 1 && console.groupCollapsed) {