+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()
+ })
})
})
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()
`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
const newProps = n2.props || EMPTY_OBJ
if (newProps.vnodeBeforeUpdate != null) {
- invokeDirectiveHook(newProps.vnodeBeforeUpdate, parentComponent, n2)
+ invokeDirectiveHook(newProps.vnodeBeforeUpdate, parentComponent, n2, n1)
}
if (patchFlag) {
if (newProps.vnodeUpdated != null) {
queuePostFlushCb(() => {
- invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2)
+ invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1)
})
}
}
} from './component'
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
-interface DirectiveBinding {
+export interface DirectiveBinding {
instance: ComponentRenderProxy | null
value?: any
oldValue?: any
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>
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)
}
}
}
-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,
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.`)
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(
}
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)
export { RendererOptions } from './createRenderer'
export { Slot, Slots } from './componentSlots'
export { PropType, ComponentPropsOptions } from './componentProps'
+export { Directive, DirectiveBinding, DirectiveHook } from './directives'
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
}
}
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) {