From 93f1339e629e600990e72f0a5e0e4ca808b51d8e Mon Sep 17 00:00:00 2001 From: simonyang08 Date: Fri, 28 Aug 2026 00:23:12 +0800 Subject: [PATCH 1/3] fix(types): support generic SFC findComponent --- src/baseWrapper.ts | 3 +++ src/interfaces/wrapperLike.ts | 4 ++++ test-dts/genericComponent.d-test.ts | 10 ++++++++++ 3 files changed, 17 insertions(+) create mode 100644 test-dts/genericComponent.d-test.ts diff --git a/src/baseWrapper.ts b/src/baseWrapper.ts index c3d146bb4..ae937ebc5 100644 --- a/src/baseWrapper.ts +++ b/src/baseWrapper.ts @@ -133,6 +133,9 @@ export default abstract class BaseWrapper< ? CreateComponentPublicInstance : VueWrapper > + // Generic SFCs emitted by vue-tsc have a generic call signature instead of + // the construct signature used by DefinedComponent. + findComponent(...args: any[]) => VNode>(selector: T): VueWrapper // searching for component created via defineComponent results in VueWrapper of proper type findComponent( selector: T | Exclude> diff --git a/src/interfaces/wrapperLike.ts b/src/interfaces/wrapperLike.ts index 58dea395f..1cd9317be 100644 --- a/src/interfaces/wrapperLike.ts +++ b/src/interfaces/wrapperLike.ts @@ -1,5 +1,6 @@ import type { DomEventNameWithModifier } from '../constants/dom-events' import type { TriggerOptions } from '../createDomEvent' +import type { VNode } from 'vue' import type { DefinedComponent, FindAllComponentsSelector, @@ -36,6 +37,9 @@ export default interface WrapperLike { findComponent( selector: T | Exclude> ): VueWrapper> + // Generic SFCs emitted by vue-tsc have a generic call signature instead of + // the construct signature used by DefinedComponent. + findComponent(...args: any[]) => VNode>(selector: T): VueWrapper findComponent>( selector: T | string ): DOMWrapper diff --git a/test-dts/genericComponent.d-test.ts b/test-dts/genericComponent.d-test.ts new file mode 100644 index 000000000..947ab671b --- /dev/null +++ b/test-dts/genericComponent.d-test.ts @@ -0,0 +1,10 @@ +import { expectType } from './index' +import type { VueWrapper } from '../src' +import { type VNode, defineComponent } from 'vue' +import { mount } from '../src' + +declare const GenericSelect: (props: { items: Item[] }) => VNode + +const wrapper = mount(defineComponent({ template: '' })) +const result = wrapper.findComponent(GenericSelect) +expectType(result) From fea3b56ba7b45a3e6626418996c6e6fc644e6f97 Mon Sep 17 00:00:00 2001 From: simonyang08 Date: Thu, 3 Sep 2026 12:26:29 +0800 Subject: [PATCH 2/3] fix(types): discriminate vue-tsc generic SFCs via __ctx return-type key Address cexbrayat's review of PR #2934: 1. Tighten the generic SFC overload so it no longer matches ordinary generic functional components. The overload's return type uses a conditional on '"__ctx" extends keyof ReturnType' to resolve to VueWrapper only when vue-tsc's __ctx discriminator is present in the selector's return type, with an 'unknown extends ReturnType' guard ahead of it because Vue's FunctionalComponent call signature returns `any`, which would otherwise satisfy the discriminator and wrongly resolve to VueWrapper. Plain generic functional components and FunctionalComponent selectors fall through to DOMWrapper, so .vm no longer type-checks on them. 2. Apply the same discriminated overload to getComponent and findAllComponents so all three APIs handle vue-tsc generic SFCs consistently. 3. Extend test-dts/genericComponent.d-test.ts to cover all three APIs for vue-tsc-shaped generic SFCs (assert VueWrapper), plain generic functional components and FunctionalComponent-typed components (assert DOMWrapper + @ts-expect-error on .vm), plus WrapperLike-level assertions mirroring the BaseWrapper overloads. Signed-off-by: simonyang08 --- src/baseWrapper.ts | 33 ++++++++- src/interfaces/wrapperLike.ts | 34 ++++++++- test-dts/genericComponent.d-test.ts | 106 ++++++++++++++++++++++++++-- 3 files changed, 163 insertions(+), 10 deletions(-) diff --git a/src/baseWrapper.ts b/src/baseWrapper.ts index ae937ebc5..962e52c48 100644 --- a/src/baseWrapper.ts +++ b/src/baseWrapper.ts @@ -134,8 +134,17 @@ export default abstract class BaseWrapper< : VueWrapper > // Generic SFCs emitted by vue-tsc have a generic call signature instead of - // the construct signature used by DefinedComponent. - findComponent(...args: any[]) => VNode>(selector: T): VueWrapper + // the construct signature used by DefinedComponent. The discriminator is the + // `__ctx` property on the return type that vue-tsc adds and ordinary generic + // functional components do not have; if absent, fall back to the DOMWrapper + // path below so that `.vm` does not type-check. + findComponent VNode>( + selector: T + ): unknown extends ReturnType + ? DOMWrapper + : '__ctx' extends keyof ReturnType + ? VueWrapper + : DOMWrapper // searching for component created via defineComponent results in VueWrapper of proper type findComponent( selector: T | Exclude> @@ -198,6 +207,16 @@ export default abstract class BaseWrapper< findAllComponents( selector: T | Exclude> ): VueWrapper>[] + // See findComponent above: vue-tsc generic SFCs are discriminated by `__ctx` in + // the return type. A plain generic functional component falls through to the + // FunctionalComponent overloads below. + findAllComponents VNode>( + selector: T + ): unknown extends ReturnType + ? DOMWrapper[] + : '__ctx' extends keyof ReturnType + ? VueWrapper[] + : DOMWrapper[] findAllComponents>( selector: T ): DOMWrapper[] @@ -305,6 +324,16 @@ export default abstract class BaseWrapper< getComponent( selector: T | Exclude> ): Omit>, 'exists'> + // vue-tsc generic SFCs share the same `__ctx` discriminator as findComponent. + // A plain generic functional component must keep resolving to DOMWrapper so that + // accessing `.vm` does not type-check at runtime. + getComponent VNode>( + selector: T + ): unknown extends ReturnType + ? Omit, 'exists'> + : '__ctx' extends keyof ReturnType + ? Omit + : Omit, 'exists'> // searching for functional component results in DOMWrapper getComponent>( selector: T | string diff --git a/src/interfaces/wrapperLike.ts b/src/interfaces/wrapperLike.ts index 1cd9317be..a64190544 100644 --- a/src/interfaces/wrapperLike.ts +++ b/src/interfaces/wrapperLike.ts @@ -37,9 +37,17 @@ export default interface WrapperLike { findComponent( selector: T | Exclude> ): VueWrapper> - // Generic SFCs emitted by vue-tsc have a generic call signature instead of - // the construct signature used by DefinedComponent. - findComponent(...args: any[]) => VNode>(selector: T): VueWrapper + // vue-tsc emits generic SFCs as call signatures returning `VNode & { __ctx?: ... }`, + // not as `DefineComponent` constructs. The `__ctx` property is the discriminator: + // a plain generic functional component returns bare `VNode` and must keep returning + // `DOMWrapper` so `.vm` does not type-check. + findComponent VNode>( + selector: T + ): unknown extends ReturnType + ? DOMWrapper + : '__ctx' extends keyof ReturnType + ? VueWrapper + : DOMWrapper findComponent>( selector: T | string ): DOMWrapper @@ -55,6 +63,16 @@ export default interface WrapperLike { findAllComponents( selector: T | Exclude> ): VueWrapper>[] + // See findComponent above: vue-tsc generic SFCs are discriminated by `__ctx` in + // the return type. A plain generic functional component falls through to the + // FunctionalComponent overloads below. + findAllComponents VNode>( + selector: T + ): unknown extends ReturnType + ? DOMWrapper[] + : '__ctx' extends keyof ReturnType + ? VueWrapper[] + : DOMWrapper[] findAllComponents>( selector: string ): DOMWrapper[] @@ -84,6 +102,16 @@ export default interface WrapperLike { getComponent( selector: T | Exclude> ): Omit>, 'exists'> + // vue-tsc generic SFCs share the same `__ctx` discriminator as findComponent. + // A plain generic functional component must keep resolving to DOMWrapper so that + // accessing `.vm` does not type-check at runtime. + getComponent VNode>( + selector: T + ): unknown extends ReturnType + ? Omit, 'exists'> + : '__ctx' extends keyof ReturnType + ? Omit + : Omit, 'exists'> // searching for functional component results in DOMWrapper getComponent>( selector: T | string diff --git a/test-dts/genericComponent.d-test.ts b/test-dts/genericComponent.d-test.ts index 947ab671b..b35196129 100644 --- a/test-dts/genericComponent.d-test.ts +++ b/test-dts/genericComponent.d-test.ts @@ -1,10 +1,106 @@ import { expectType } from './index' -import type { VueWrapper } from '../src' -import { type VNode, defineComponent } from 'vue' +import type { FunctionalComponent, VNode } from 'vue' +import { defineComponent, h } from 'vue' import { mount } from '../src' +import type { DOMWrapper, VueWrapper } from '../src' +import type WrapperLike from '../src/interfaces/wrapperLike' -declare const GenericSelect: (props: { items: Item[] }) => VNode +// `vue-tsc` emits a generic SFC as a function with a generic call signature whose +// return type is `VNode & { __ctx?: ... }`. The `__ctx` property is the +// discriminator between vue-tsc generic SFCs and ordinary generic functional +// components (which return bare `VNode`). +declare const VueTscGenericSfc: ( + __VLS_props: any, + __VLS_ctx?: any, + __VLS_expose?: any, + __VLS_setup?: Promise +) => VNode & { __ctx?: any } + +// A plain generic functional component returns bare `VNode` (no `__ctx`). +declare const GenericFunctional: (props: { items: T[] }) => VNode const wrapper = mount(defineComponent({ template: '' })) -const result = wrapper.findComponent(GenericSelect) -expectType(result) + +// ---- findComponent ---- +// Vue-tsc generic SFC should resolve to VueWrapper, not DOMWrapper. +const sfcFound = wrapper.findComponent(VueTscGenericSfc) +expectType(sfcFound) +// `.vm` is a VueWrapper-only API; it must type-check on the vue-tsc SFC result. +expectType(sfcFound.vm) + +// Plain generic functional component must still resolve to DOMWrapper; `.vm` +// should NOT type-check (use @ts-expect-error to assert this). +const fnFound = wrapper.findComponent(GenericFunctional) +expectType>(fnFound) +void ( + // @ts-expect-error -- DOMWrapper has no `vm` property. + fnFound.vm +) + +// ---- getComponent ---- +const sfcGot = wrapper.getComponent(VueTscGenericSfc) +expectType>(sfcGot) +void ( + // @ts-expect-error -- `exists` is stripped on getComponent. + sfcGot.exists +) +expectType(sfcGot.vm) + +const fnGot = wrapper.getComponent(GenericFunctional) +expectType, 'exists'>>(fnGot) +void ( + // @ts-expect-error -- DOMWrapper has no `vm` property. + fnGot.vm +) + +// ---- findAllComponents ---- +const sfcAll = wrapper.findAllComponents(VueTscGenericSfc) +expectType(sfcAll) +expectType(sfcAll[0]?.vm) + +const fnAll = wrapper.findAllComponents(GenericFunctional) +expectType[]>(fnAll) +void ( + // @ts-expect-error -- DOMWrapper[] elements have no `vm` property. + fnAll[0]?.vm +) + +// A `FunctionalComponent`-typed component has a call signature returning `any` +// (see Vue's FunctionalComponent type). The `any` return must not satisfy the +// `__ctx` discriminator, so all three APIs keep resolving to DOMWrapper and +// `.vm` must not type-check. +const PlainFunctional: FunctionalComponent<{ a: string }> = props => + h('div', props.a) + +// ---- findComponent (FunctionalComponent shape) ---- +const fcFound = wrapper.findComponent(PlainFunctional) +expectType>(fcFound) +void ( + // @ts-expect-error -- DOMWrapper has no `vm` property. + fcFound.vm +) + +// ---- getComponent (FunctionalComponent shape) ---- +const fcGot = wrapper.getComponent(PlainFunctional) +expectType, 'exists'>>(fcGot) +void ( + // @ts-expect-error -- DOMWrapper has no `vm` property. + fcGot.vm +) + +// ---- findAllComponents (FunctionalComponent shape) ---- +const fcAll = wrapper.findAllComponents(PlainFunctional) +expectType[]>(fcAll) +void ( + // @ts-expect-error -- DOMWrapper[] elements have no `vm` property. + fcAll[0]?.vm +) + +// The WrapperLike interface mirrors the BaseWrapper overloads, including the +// `any`-return guard. +const wrapperLike: WrapperLike = wrapper +expectType>(wrapperLike.findComponent(PlainFunctional)) +expectType[]>(wrapperLike.findAllComponents(PlainFunctional)) +expectType, 'exists'>>( + wrapperLike.getComponent(PlainFunctional) +) From 7bd2bef773e07d4a73f33155e8a0bbc729f155e3 Mon Sep 17 00:00:00 2001 From: simonyang08 Date: Fri, 4 Sep 2026 10:40:11 +0800 Subject: [PATCH 3/3] test(types): use real generic SFC in genericComponent.d-test Address cexbrayat's review of PR #2934: Replace the hand-written VueTscGenericSfc declaration in test-dts/genericComponent.d-test.ts with a real + + diff --git a/test-dts/genericComponent.d-test.ts b/test-dts/genericComponent.d-test.ts index b35196129..0ccf04585 100644 --- a/test-dts/genericComponent.d-test.ts +++ b/test-dts/genericComponent.d-test.ts @@ -5,16 +5,13 @@ import { mount } from '../src' import type { DOMWrapper, VueWrapper } from '../src' import type WrapperLike from '../src/interfaces/wrapperLike' -// `vue-tsc` emits a generic SFC as a function with a generic call signature whose -// return type is `VNode & { __ctx?: ... }`. The `__ctx` property is the -// discriminator between vue-tsc generic SFCs and ordinary generic functional -// components (which return bare `VNode`). -declare const VueTscGenericSfc: ( - __VLS_props: any, - __VLS_ctx?: any, - __VLS_expose?: any, - __VLS_setup?: Promise -) => VNode & { __ctx?: any } +// Real generic SFC. `vue-tsc` emits a generic call signature whose return +// type carries a `__ctx` property; that is the discriminator the overload +// in `src/baseWrapper.ts` keys on to pick `VueWrapper` over `DOMWrapper`. +// Resolved by `vue-tsc` via the `tsd` script (see package.json) so the type +// below is the actual shape vue-tsc would emit for a user component, not +// a hand-written mock. +import GenericSfc from './GenericSfc.vue' // A plain generic functional component returns bare `VNode` (no `__ctx`). declare const GenericFunctional: (props: { items: T[] }) => VNode @@ -22,8 +19,8 @@ declare const GenericFunctional: (props: { items: T[] }) => VNode const wrapper = mount(defineComponent({ template: '' })) // ---- findComponent ---- -// Vue-tsc generic SFC should resolve to VueWrapper, not DOMWrapper. -const sfcFound = wrapper.findComponent(VueTscGenericSfc) +// vue-tsc generic SFC should resolve to VueWrapper, not DOMWrapper. +const sfcFound = wrapper.findComponent(GenericSfc) expectType(sfcFound) // `.vm` is a VueWrapper-only API; it must type-check on the vue-tsc SFC result. expectType(sfcFound.vm) @@ -38,7 +35,7 @@ void ( ) // ---- getComponent ---- -const sfcGot = wrapper.getComponent(VueTscGenericSfc) +const sfcGot = wrapper.getComponent(GenericSfc) expectType>(sfcGot) void ( // @ts-expect-error -- `exists` is stripped on getComponent. @@ -54,7 +51,7 @@ void ( ) // ---- findAllComponents ---- -const sfcAll = wrapper.findAllComponents(VueTscGenericSfc) +const sfcAll = wrapper.findAllComponents(GenericSfc) expectType(sfcAll) expectType(sfcAll[0]?.vm)