From 50a05047ff373e457485fff731da11b3f012b8f6 Mon Sep 17 00:00:00 2001
From: eddyerburgh <edward.yerburgh@gmail.com>
Date: Wed, 27 Jun 2018 19:56:52 +0100
Subject: [PATCH] fix: stubs extended component correctly

---
 packages/shared/stub-components.js            | 88 ++++++++++++-------
 .../test-utils/src/find-vue-components.js     |  2 +-
 test/specs/shallow-mount.spec.js              | 17 ++++
 test/specs/wrapper/find.spec.js               | 14 +++
 4 files changed, 90 insertions(+), 31 deletions(-)

diff --git a/packages/shared/stub-components.js b/packages/shared/stub-components.js
index 347ff4d8f..57220755b 100644
--- a/packages/shared/stub-components.js
+++ b/packages/shared/stub-components.js
@@ -2,7 +2,12 @@
 
 import Vue from 'vue'
 import { compileToFunctions } from 'vue-template-compiler'
-import { throwError } from './util'
+import {
+  throwError,
+  camelize,
+  capitalize,
+  hyphenate
+} from './util'
 import {
   componentNeedsCompiling,
   templateContainsComponent
@@ -21,28 +26,37 @@ function isValidStub (stub: any) {
   )
 }
 
+function resolveComponent (obj, component) {
+  return obj[component] ||
+    obj[hyphenate(component)] ||
+    obj[camelize(component)] ||
+    obj[capitalize(camelize(component))] ||
+    obj[capitalize(component)] ||
+    {}
+}
+
 function isRequiredComponent (name) {
   return (
     name === 'KeepAlive' || name === 'Transition' || name === 'TransitionGroup'
   )
 }
 
-function getCoreProperties (component: Component): Object {
+function getCoreProperties (componentOptions: Component): Object {
   return {
-    attrs: component.attrs,
-    name: component.name,
-    on: component.on,
-    key: component.key,
-    ref: component.ref,
-    props: component.props,
-    domProps: component.domProps,
-    class: component.class,
-    staticClass: component.staticClass,
-    staticStyle: component.staticStyle,
-    style: component.style,
-    normalizedStyle: component.normalizedStyle,
-    nativeOn: component.nativeOn,
-    functional: component.functional
+    attrs: componentOptions.attrs,
+    name: componentOptions.name,
+    on: componentOptions.on,
+    key: componentOptions.key,
+    ref: componentOptions.ref,
+    props: componentOptions.props,
+    domProps: componentOptions.domProps,
+    class: componentOptions.class,
+    staticClass: componentOptions.staticClass,
+    staticStyle: componentOptions.staticStyle,
+    style: componentOptions.style,
+    normalizedStyle: componentOptions.normalizedStyle,
+    nativeOn: componentOptions.nativeOn,
+    functional: componentOptions.functional
   }
 }
 function createStubFromString (
@@ -62,24 +76,31 @@ function createStubFromString (
     throwError('options.stub cannot contain a circular reference')
   }
 
+  const componentOptions = typeof originalComponent === 'function'
+    ? originalComponent.extendOptions
+    : originalComponent
+
   return {
-    ...getCoreProperties(originalComponent),
+    ...getCoreProperties(componentOptions),
     ...compileToFunctions(templateString)
   }
 }
 
-function createBlankStub (originalComponent: Component) {
-  const name = `${originalComponent.name}-stub`
+function createBlankStub (originalComponent: Component, name: string) {
+  const componentOptions = typeof originalComponent === 'function'
+    ? originalComponent.extendOptions
+    : originalComponent
+  const tagName = `${name}-stub`
 
   // ignoreElements does not exist in Vue 2.0.x
   if (Vue.config.ignoredElements) {
-    Vue.config.ignoredElements.push(name)
+    Vue.config.ignoredElements.push(tagName)
   }
 
   return {
-    ...getCoreProperties(originalComponent),
+    ...getCoreProperties(componentOptions),
     render (h) {
-      return h(name)
+      return h(tagName)
     }
   }
 }
@@ -101,7 +122,9 @@ export function createComponentStubs (
       if (typeof stub !== 'string') {
         throwError(`each item in an options.stubs array must be a ` + `string`)
       }
-      components[stub] = createBlankStub({ name: stub })
+      const component = resolveComponent(originalComponents, stub)
+
+      components[stub] = createBlankStub(component, stub)
     })
   } else {
     Object.keys(stubs).forEach(stub => {
@@ -114,7 +137,8 @@ export function createComponentStubs (
         )
       }
       if (stubs[stub] === true) {
-        components[stub] = createBlankStub({ name: stub })
+        const component = resolveComponent(originalComponents, stub)
+        components[stub] = createBlankStub(component, stub)
         return
       }
 
@@ -162,12 +186,16 @@ export function createComponentStubs (
 
 function stubComponents (components: Object, stubbedComponents: Object) {
   Object.keys(components).forEach(component => {
+    const cmp = components[component]
+    const componentOptions = typeof cmp === 'function'
+      ? cmp.extendOptions
+      : cmp
     // Remove cached constructor
-    delete components[component]._Ctor
-    if (!components[component].name) {
-      components[component].name = component
+    delete componentOptions._Ctor
+    if (!componentOptions.name) {
+      componentOptions.name = component
     }
-    stubbedComponents[component] = createBlankStub(components[component])
+    stubbedComponents[component] = createBlankStub(componentOptions, component)
   })
 }
 
@@ -178,7 +206,7 @@ export function createComponentStubsForAll (component: Component): Object {
     stubComponents(component.components, stubbedComponents)
   }
 
-  stubbedComponents[component.name] = createBlankStub(component)
+  stubbedComponents[component.name] = createBlankStub(component, component.name)
 
   let extended = component.extends
 
@@ -204,7 +232,7 @@ export function createComponentStubsForGlobals (instance: Component): Object {
       return
     }
 
-    components[c] = createBlankStub(instance.options.components[c])
+    components[c] = createBlankStub(instance.options.components[c], c)
     delete instance.options.components[c]._Ctor
     delete components[c]._Ctor
   })
diff --git a/packages/test-utils/src/find-vue-components.js b/packages/test-utils/src/find-vue-components.js
index 54c936d9a..89ba6af6e 100644
--- a/packages/test-utils/src/find-vue-components.js
+++ b/packages/test-utils/src/find-vue-components.js
@@ -106,7 +106,7 @@ export default function findVueComponents (
     )
   }
   const nameSelector =
-    typeof selector === 'function' ? selector.options.name : selector.name
+    typeof selector === 'function' ? selector.extendOptions.name : selector.name
   const components = root._isVue
     ? findAllVueComponentsFromVm(root)
     : findAllVueComponentsFromVnode(root)
diff --git a/test/specs/shallow-mount.spec.js b/test/specs/shallow-mount.spec.js
index 6522a85e6..970230cef 100644
--- a/test/specs/shallow-mount.spec.js
+++ b/test/specs/shallow-mount.spec.js
@@ -239,6 +239,23 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'shallowMount', () => {
     ).to.equal(3)
   })
 
+  it('handles extended stubs', () => {
+    const ChildComponent = Vue.extend({
+      template: '<div />',
+      props: ['propA']
+    })
+    const TestComponent = {
+      template: '<child-component propA="hey" />',
+      components: { ChildComponent }
+    }
+    const wrapper = shallowMount(TestComponent, {
+      stubs: ['child-component']
+    })
+
+    expect(wrapper.find(ChildComponent).vm.propA)
+      .to.equal('hey')
+  })
+
   it('throws an error when the component fails to mount', () => {
     expect(() =>
       shallowMount({
diff --git a/test/specs/wrapper/find.spec.js b/test/specs/wrapper/find.spec.js
index ae2fb7e62..c75240f15 100644
--- a/test/specs/wrapper/find.spec.js
+++ b/test/specs/wrapper/find.spec.js
@@ -1,5 +1,6 @@
 import { compileToFunctions } from 'vue-template-compiler'
 import { createLocalVue } from '~vue/test-utils'
+import Vue from 'vue'
 import ComponentWithChild from '~resources/components/component-with-child.vue'
 import ComponentWithoutName from '~resources/components/component-without-name.vue'
 import ComponentWithSlots from '~resources/components/component-with-slots.vue'
@@ -79,6 +80,19 @@ describeWithShallowAndMount('find', mountingMethod => {
     expect(wrapper.find('#foo').vnode).to.be.an('object')
   })
 
+  it('returns matching extended component', () => {
+    const ChildComponent = Vue.extend({
+      template: '<div />',
+      props: ['propA']
+    })
+    const TestComponent = {
+      template: '<child-component propA="hey" />',
+      components: { ChildComponent }
+    }
+    const wrapper = mountingMethod(TestComponent)
+    expect(wrapper.find(ChildComponent).vnode).to.be.an('object')
+  })
+
   it('returns Wrapper of elements matching attribute selector passed', () => {
     const compiled = compileToFunctions('<div><a href="/"></a></div>')
     const wrapper = mountingMethod(compiled)