From aac807bc63f961bf72edab06bec4481ac0b83c38 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 2 Sep 2019 12:09:29 -0400 Subject: [PATCH] test: test for directives --- .../runtime-core/__tests__/directives.spec.ts | 145 +++++++++++++++++- .../__tests__/errorHandling.spec.ts | 12 ++ packages/runtime-core/src/createRenderer.ts | 4 +- packages/runtime-core/src/directives.ts | 46 +++--- packages/runtime-core/src/errorHandling.ts | 7 +- packages/runtime-core/src/index.ts | 1 + packages/runtime-core/src/vnode.ts | 16 +- packages/runtime-core/src/warning.ts | 8 +- 8 files changed, 203 insertions(+), 36 deletions(-) diff --git a/packages/runtime-core/__tests__/directives.spec.ts b/packages/runtime-core/__tests__/directives.spec.ts index fdcc2350..6cb43e72 100644 --- a/packages/runtime-core/__tests__/directives.spec.ts +++ b/packages/runtime-core/__tests__/directives.spec.ts @@ -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() + }) }) diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts index 9756692f..6d346cdf 100644 --- a/packages/runtime-core/__tests__/errorHandling.spec.ts +++ b/packages/runtime-core/__tests__/errorHandling.spec.ts @@ -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 diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 7a1dc92d..7ad882b8 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -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) }) } } diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index 8e3bd6f2..8e620b9d 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -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 @@ -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( diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 0f02732d..53cadc48 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -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) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index f555bb98..dba7dbae 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 1fa122f5..84f4032e 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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 } } diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index 3a8e59a4..b8301ba3 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -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) {