]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: test for directives
authorEvan You <yyx990803@gmail.com>
Mon, 2 Sep 2019 16:09:29 +0000 (12:09 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 2 Sep 2019 16:09:29 +0000 (12:09 -0400)
packages/runtime-core/__tests__/directives.spec.ts
packages/runtime-core/__tests__/errorHandling.spec.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/directives.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/vnode.ts
packages/runtime-core/src/warning.ts

index fdcc2350ef23a0c068c3ed856a97a85df939c83d..6cb43e7297b49cca02e65a1059a22ffc2044e3da 100644 (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()
+  })
 })
index 9756692fcb1f54add531cfc948c03eddfae530ac..6d346cdf8dcbba37e4ad47dd08f3eee4f40d24b0 100644 (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
index 7a1dc92d4def208e308e2dd647107fd980e2d81f..7ad882b8103131541e3e1522fa2b3bc83a97bc44 100644 (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)
       })
     }
   }
index 8e3bd6f2d98e1cc8947546fb7e9cd47e2fee7cdd..8e620b9dec86474749a606e0358461d968a4a052 100644 (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(
index 0f02732d8d773fab1300fa83c33ec8f90dc9e22e..53cadc481adb3f0786e4f54210642a9ac63f3a02 100644 (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)
index f555bb9806fc8557957bdbbd8c2fa5645bc9580d..dba7dbae066fa950a129e7eb60b828f23f31dfee 100644 (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'
index 1fa122f58e4abb60d27527dd4364886668fc920b..84f4032eec34a8850a17dea2fde620d7b832dc80 100644 (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
   }
 }
 
index 3a8e59a4ffc75b810edeced4346098228477485b..b8301ba3462e01473e805188c6ce9ac1d46c2b8a 100644 (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) {