Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions src/vnodeTransformers/stubComponentsTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
defineComponent,
h
} from 'vue'
import { camelize, hyphenate } from '../utils/vueShared'
import { camelize, capitalize, hyphenate } from '../utils/vueShared'
import { matchName } from '../utils/matchName'
import { isComponent, isFunctionalComponent } from '../utils'
import { unwrapLegacyVueExtendComponent } from '../utils/vueCompatSupport'
Expand Down Expand Up @@ -211,6 +211,30 @@ export function createStubComponentsTransformer({
shallow = false,
renderStubDefaultSlot = false
}: CreateStubComponentsTransformerConfig): VTUVNodeTypeTransformer {
// `app.component()` registrations (e.g. from a plugin) can use a key that
// differs from the component name, like naive-ui's `NButton` (name `Button`)
const globalStubKeys = new WeakMap<object, Map<unknown, string>>()
const getGlobalStubKey = (
registry: Record<string, Component>,
type: VNodeTypes
) => {
let stubKeys = globalStubKeys.get(registry)
if (!stubKeys) {
stubKeys = new Map()
for (const stubKey in stubs) {
for (const key of [
stubKey,
capitalize(camelize(stubKey)),
hyphenate(stubKey)
]) {
stubKeys.set(registry[key], stubKey)
}
}
globalStubKeys.set(registry, stubKeys)
}
return stubKeys.get(type)
}

return function componentsTransformer(type, instance) {
for (const tag in DEFAULT_STUBS) {
const predicate = DEFAULT_STUBS[tag as keyof typeof DEFAULT_STUBS]
Expand All @@ -237,8 +261,20 @@ export function createStubComponentsTransformer({
}
}

// Prio 2 using the name attribute in the component
if (!stub && componentName) {
// Prio 2 using the key in globally registered components
if (stub == null && instance) {
const globalStubKey = getGlobalStubKey(
instance.appContext.components,
type
)
if (globalStubKey) {
stub = stubs[globalStubKey]
name = globalStubKey
}
}

// Prio 3 using the name attribute in the component
if (stub == null && componentName) {
stub = resolveComponentStubByName(componentName, stubs)
if (stub) {
name = componentName
Expand Down
60 changes: 60 additions & 0 deletions tests/mountingOptions/global.stubs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,66 @@ describe('mounting options: stubs', () => {
expect(wrapper.html()).toBe('<span>MyComponentKey stubbed</span>')
})

it('stubs a component by the key it is globally registered with', () => {
const TestComponent = defineComponent({
template: '<div><n-button /><NButton /><n-icon /><nTag /></div>'
})

const wrapper = mount(TestComponent, {
global: {
plugins: [
{
install(app) {
app.component('NButton', {
name: 'Button',
template: '<button />'
})
app.component('n-icon', { name: 'Icon', template: '<i />' })
app.component('nTag', { name: 'Tag', template: '<b />' })
}
}
],
stubs: { 'n-button': true, NIcon: true, nTag: true }
}
})

expect(wrapper.html()).toBe(
'<div>\n' +
' <n-button-stub></n-button-stub>\n' +
' <n-button-stub></n-button-stub>\n' +
' <n-icon-stub></n-icon-stub>\n' +
' <n-tag-stub></n-tag-stub>\n' +
'</div>'
)
})

it('opts out of stubbing a component by its registered key with shallow', () => {
const Button = defineComponent({
name: 'Button',
template: '<button>real button</button>'
})
const Card = defineComponent({
name: 'Card',
template: '<section>real card</section>'
})

const TestComponent = defineComponent({
components: { LocalCard: Card },
template: '<div><NButton /><LocalCard /></div>'
})

const wrapper = mount(TestComponent, {
shallow: true,
global: {
plugins: [{ install: app => app.component('NButton', Button) }],
stubs: { NButton: false, LocalCard: false }
}
})

expect(wrapper.html()).toContain('<button>real button</button>')
expect(wrapper.html()).toContain('<section>real card</section>')
})

describe('stub async component', () => {
const AsyncComponentWithoutName = defineAsyncComponent(async () => ({
template: '<span>AsyncComponent</span>'
Expand Down
Loading