Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
32 changes: 32 additions & 0 deletions src/baseWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ export default abstract class BaseWrapper<
? CreateComponentPublicInstance<Props, RawBindings, D, C, M>
: VueWrapper<CreateComponentPublicInstance>
>
// Generic SFCs emitted by vue-tsc have a generic call signature instead of
// 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<T extends (...args: any[]) => VNode>(
selector: T
): unknown extends ReturnType<T>
? DOMWrapper<Node>
: '__ctx' extends keyof ReturnType<T>
? VueWrapper
: DOMWrapper<Node>
// searching for component created via defineComponent results in VueWrapper of proper type
findComponent<T extends DefinedComponent>(
selector: T | Exclude<FindComponentSelector, FunctionalComponent<any>>
Expand Down Expand Up @@ -195,6 +207,16 @@ export default abstract class BaseWrapper<
findAllComponents<T extends DefinedComponent>(
selector: T | Exclude<FindAllComponentsSelector, FunctionalComponent<any>>
): VueWrapper<InstanceType<T>>[]
// 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<T extends (...args: any[]) => VNode>(
selector: T
): unknown extends ReturnType<T>
? DOMWrapper<Node>[]
: '__ctx' extends keyof ReturnType<T>
? VueWrapper[]
: DOMWrapper<Node>[]
findAllComponents<T extends FunctionalComponent<any>>(
selector: T
): DOMWrapper<Node>[]
Expand Down Expand Up @@ -302,6 +324,16 @@ export default abstract class BaseWrapper<
getComponent<T extends DefinedComponent>(
selector: T | Exclude<FindComponentSelector, FunctionalComponent<any>>
): Omit<VueWrapper<InstanceType<T>>, '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<T extends (...args: any[]) => VNode>(
selector: T
): unknown extends ReturnType<T>
? Omit<DOMWrapper<Element>, 'exists'>
: '__ctx' extends keyof ReturnType<T>
? Omit<VueWrapper, 'exists'>
: Omit<DOMWrapper<Element>, 'exists'>
// searching for functional component results in DOMWrapper
getComponent<T extends FunctionalComponent<any>>(
selector: T | string
Expand Down
32 changes: 32 additions & 0 deletions src/interfaces/wrapperLike.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -36,6 +37,17 @@ export default interface WrapperLike {
findComponent<T extends DefinedComponent>(
selector: T | Exclude<FindComponentSelector, FunctionalComponent<any>>
): VueWrapper<InstanceType<T>>
// 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<T extends (...args: any[]) => VNode>(
selector: T
): unknown extends ReturnType<T>
? DOMWrapper<Element>
: '__ctx' extends keyof ReturnType<T>
? VueWrapper
: DOMWrapper<Element>
findComponent<T extends FunctionalComponent<any>>(
selector: T | string
): DOMWrapper<Element>
Expand All @@ -51,6 +63,16 @@ export default interface WrapperLike {
findAllComponents<T extends DefinedComponent>(
selector: T | Exclude<FindAllComponentsSelector, FunctionalComponent<any>>
): VueWrapper<InstanceType<T>>[]
// 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<T extends (...args: any[]) => VNode>(
selector: T
): unknown extends ReturnType<T>
? DOMWrapper<Node>[]
: '__ctx' extends keyof ReturnType<T>
? VueWrapper[]
: DOMWrapper<Node>[]
findAllComponents<T extends FunctionalComponent<any>>(
selector: string
): DOMWrapper<Element>[]
Expand Down Expand Up @@ -80,6 +102,16 @@ export default interface WrapperLike {
getComponent<T extends DefinedComponent>(
selector: T | Exclude<FindComponentSelector, FunctionalComponent<any>>
): Omit<VueWrapper<InstanceType<T>>, '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<T extends (...args: any[]) => VNode>(
selector: T
): unknown extends ReturnType<T>
? Omit<DOMWrapper<Element>, 'exists'>
: '__ctx' extends keyof ReturnType<T>
? Omit<VueWrapper, 'exists'>
: Omit<DOMWrapper<Element>, 'exists'>
// searching for functional component results in DOMWrapper
getComponent<T extends FunctionalComponent<any>>(
selector: T | string
Expand Down
106 changes: 106 additions & 0 deletions test-dts/genericComponent.d-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expectType } from './index'
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'

// `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: <T extends string | number>(
__VLS_props: any,
__VLS_ctx?: any,
__VLS_expose?: any,
__VLS_setup?: Promise<any>
) => VNode & { __ctx?: any }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to use a real <script setup generic="Item"> component in a .vue file imported by the test, instead of relying on internals of vue-tsc


// A plain generic functional component returns bare `VNode` (no `__ctx`).
declare const GenericFunctional: <T>(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)
expectType<VueWrapper>(sfcFound)
// `.vm` is a VueWrapper-only API; it must type-check on the vue-tsc SFC result.
expectType<unknown>(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<DOMWrapper<Node>>(fnFound)
void (
// @ts-expect-error -- DOMWrapper has no `vm` property.
fnFound.vm
)

// ---- getComponent ----
const sfcGot = wrapper.getComponent(VueTscGenericSfc)
expectType<Omit<VueWrapper, 'exists'>>(sfcGot)
void (
// @ts-expect-error -- `exists` is stripped on getComponent.
sfcGot.exists
)
expectType<unknown>(sfcGot.vm)

const fnGot = wrapper.getComponent(GenericFunctional)
expectType<Omit<DOMWrapper<Element>, 'exists'>>(fnGot)
void (
// @ts-expect-error -- DOMWrapper has no `vm` property.
fnGot.vm
)

// ---- findAllComponents ----
const sfcAll = wrapper.findAllComponents(VueTscGenericSfc)
expectType<VueWrapper[]>(sfcAll)
expectType<unknown | undefined>(sfcAll[0]?.vm)

const fnAll = wrapper.findAllComponents(GenericFunctional)
expectType<DOMWrapper<Node>[]>(fnAll)
void (
// @ts-expect-error -- DOMWrapper<Node>[] 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<DOMWrapper<Node>>(fcFound)
void (
// @ts-expect-error -- DOMWrapper has no `vm` property.
fcFound.vm
)

// ---- getComponent (FunctionalComponent shape) ----
const fcGot = wrapper.getComponent(PlainFunctional)
expectType<Omit<DOMWrapper<Element>, 'exists'>>(fcGot)
void (
// @ts-expect-error -- DOMWrapper has no `vm` property.
fcGot.vm
)

// ---- findAllComponents (FunctionalComponent shape) ----
const fcAll = wrapper.findAllComponents(PlainFunctional)
expectType<DOMWrapper<Node>[]>(fcAll)
void (
// @ts-expect-error -- DOMWrapper<Node>[] 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<DOMWrapper<Element>>(wrapperLike.findComponent(PlainFunctional))
expectType<DOMWrapper<Node>[]>(wrapperLike.findAllComponents(PlainFunctional))
expectType<Omit<DOMWrapper<Element>, 'exists'>>(
wrapperLike.getComponent(PlainFunctional)
)
Loading