Skip to content

Commit

Permalink
fix: Allow find component without name in script setup by name (#1171)
Browse files Browse the repository at this point in the history
  • Loading branch information
freakzlike authored Dec 24, 2021
1 parent a46b1c4 commit 36c3d4f
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 47 deletions.
48 changes: 6 additions & 42 deletions src/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import {
} from 'vue'
import { hyphenate } from './utils/vueShared'
import { matchName } from './utils/matchName'
import { isComponent, isFunctionalComponent, isObjectComponent } from './utils'
import { isComponent, isFunctionalComponent } from './utils'
import { ComponentInternalInstance } from '@vue/runtime-core'
import {
isLegacyExtendedComponent,
unwrapLegacyVueExtendComponent
} from './utils/vueCompatSupport'
import { unwrapLegacyVueExtendComponent } from './utils/vueCompatSupport'
import { Stub, Stubs } from './types'
import {
getComponentName,
getComponentRegisteredName
} from './utils/componentName'

interface StubOptions {
name: string
Expand Down Expand Up @@ -139,43 +140,6 @@ const resolveComponentStubByName = (componentName: string, stubs: Stubs) => {
}
}

const getComponentRegisteredName = (
instance: ComponentInternalInstance | null,
type: VNodeTypes
): string | null => {
if (!instance || !instance.parent) return null

// try to infer the name based on local resolution
const registry = (instance.type as any).components
for (const key in registry) {
if (registry[key] === type) {
return key
}
}

return null
}

const getComponentName = (instance: any | null, type: VNodeTypes): string => {
if (isObjectComponent(type)) {
const defaultName = Object.keys(instance?.setupState || {}).find(
(key) => instance.setupState[key] === type
)

return defaultName || type.name || ''
}

if (isLegacyExtendedComponent(type)) {
return unwrapLegacyVueExtendComponent(type).name || ''
}

if (isFunctionalComponent(type)) {
return type.displayName || type.name
}

return ''
}

function createStubOnceForType(
type: ConcreteComponent,
factoryFn: () => ConcreteComponent,
Expand Down
52 changes: 52 additions & 0 deletions src/utils/componentName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ComponentInternalInstance } from '@vue/runtime-core'
import { VNodeTypes } from 'vue'
import { isFunctionalComponent, isObjectComponent } from '../utils'
import {
isLegacyExtendedComponent,
unwrapLegacyVueExtendComponent
} from './vueCompatSupport'

const getComponentNameInSetup = (
instance: any | null,
type: VNodeTypes
): string | undefined =>
Object.keys(instance?.setupState || {}).find(
(key) => instance.setupState[key] === type
)

export const getComponentRegisteredName = (
instance: ComponentInternalInstance | null,
type: VNodeTypes
): string | null => {
if (!instance || !instance.parent) return null

// try to infer the name based on local resolution
const registry = (instance.type as any).components
for (const key in registry) {
if (registry[key] === type) {
return key
}
}

// try to retrieve name imported in script setup
return getComponentNameInSetup(instance.parent, type) || null
}

export const getComponentName = (
instance: any | null,
type: VNodeTypes
): string => {
if (isObjectComponent(type)) {
return getComponentNameInSetup(instance, type) || type.name || ''
}

if (isLegacyExtendedComponent(type)) {
return unwrapLegacyVueExtendComponent(type).name || ''
}

if (isFunctionalComponent(type)) {
return type.displayName || type.name
}

return ''
}
14 changes: 9 additions & 5 deletions src/utils/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { isComponent } from '../utils'
import { matchName } from './matchName'
import { unwrapLegacyVueExtendComponent } from './vueCompatSupport'
import { getComponentName, getComponentRegisteredName } from './componentName'

/**
* Detect whether a selector matches a VNode
Expand Down Expand Up @@ -53,7 +54,7 @@ export function matches(
}

let componentName: string | undefined
componentName = nodeType.displayName || nodeType.name
componentName = getComponentName(node.component, nodeType)

let selectorName = selector.name

Expand All @@ -62,6 +63,9 @@ export function matches(
return matchName(selectorName, componentName)
}

componentName =
getComponentRegisteredName(node.component, nodeType) || undefined

// if a name is missing, then check the locally registered components in the parent
if (node.component.parent) {
const registry = (node.component.parent as any).type.components
Expand All @@ -75,10 +79,10 @@ export function matches(
componentName = key
}
}
// we may have one or both missing names
if (selectorName && componentName) {
return matchName(selectorName, componentName)
}
}

if (selectorName && componentName) {
return matchName(selectorName, componentName)
}

return false
Expand Down
23 changes: 23 additions & 0 deletions tests/findComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineComponent, h, nextTick } from 'vue'
import { mount } from '../src'
import Hello from './components/Hello.vue'
import ComponentWithoutName from './components/ComponentWithoutName.vue'
import ScriptSetupWithChildren from './components/ScriptSetupWithChildren.vue'

const compC = defineComponent({
name: 'ComponentC',
Expand Down Expand Up @@ -83,6 +84,28 @@ describe('findComponent', () => {
expect(wrapper.findComponent({ name: 'component-c' }).exists()).toBeTruthy()
})

it('finds component within script setup by name', () => {
const wrapper = mount(ScriptSetupWithChildren)
expect(wrapper.findComponent({ name: 'Hello' }).text()).toBe('Hello world')
expect(
wrapper.findComponent({ name: 'ComponentWithInput' }).exists()
).toBeTruthy()
expect(
wrapper.findComponent({ name: 'component-with-input' }).exists()
).toBeTruthy()
})

it('finds component within script setup without name', () => {
const wrapper = mount(ScriptSetupWithChildren)
expect(wrapper.findComponent({ name: 'ScriptSetup' }).exists()).toBeTruthy()
expect(
wrapper.findComponent({ name: 'ComponentWithoutName' }).exists()
).toBeTruthy()
expect(
wrapper.findComponent({ name: 'component-without-name' }).exists()
).toBeTruthy()
})

it('finds root component', async () => {
const Comp = defineComponent({
name: 'C',
Expand Down
28 changes: 28 additions & 0 deletions tests/mountingOptions/global.stubs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { config, flushPromises, mount, RouterLinkStub } from '../../src'
import Hello from '../components/Hello.vue'
import ComponentWithoutName from '../components/ComponentWithoutName.vue'
import ComponentWithSlots from '../components/ComponentWithSlots.vue'
import ScriptSetupWithChildren from '../components/ScriptSetupWithChildren.vue'

describe('mounting options: stubs', () => {
let configStubsSave = config.global.stubs
Expand Down Expand Up @@ -364,6 +365,33 @@ describe('mounting options: stubs', () => {
expect(wrapper.html()).toBe('<foo-bar-stub></foo-bar-stub>')
})

it('stubs components within script setup', () => {
const wrapper = mount(ScriptSetupWithChildren as any, {
global: {
stubs: {
Hello: { template: '<span>Stubbed Hello</span>' },
ComponentWithInput: {
template: '<span>Stubbed ComponentWithInput</span>'
},
ComponentWithoutName: {
template: '<span>Stubbed ComponentWithoutName</span>'
},
ComponentAsync: { template: '<span>Stubbed ComponentAsync</span>' },
ScriptSetup: { template: '<span>Stubbed ScriptSetup</span>' },
WithProps: { template: '<span>Stubbed WithProps</span>' }
}
}
})
expect(wrapper.html()).toBe(
'<span>Stubbed Hello</span>\n' +
'<span>Stubbed ComponentWithInput</span>\n' +
'<span>Stubbed ComponentWithoutName</span>\n' +
'<span>Stubbed ComponentAsync</span>\n' +
'<span>Stubbed ScriptSetup</span>\n' +
'<span>Stubbed WithProps</span>'
)
})

it('stubs transition by default', () => {
const Comp = {
template: `<transition><div id="content" /></transition>`
Expand Down

0 comments on commit 36c3d4f

Please sign in to comment.