Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"test:coverage": "vitest --coverage",
"test:watch": "vitest --watch",
"test:build": "vitest --mode test-build",
"tsd": "tsc -p test-dts/tsconfig.tsd.json",
"tsd": "vue-tsc --noEmit -p test-dts/tsconfig.tsd.json",
"tsc:docs": "vue-tsc -p docs/.vitepress/tsconfig.vitepress.json",
"build": "rollup -c rollup.config.ts --bundleConfigAsCjs",
"prepare": "rollup -c rollup.config.ts --bundleConfigAsCjs",
Expand Down
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
9 changes: 9 additions & 0 deletions test-dts/GenericSfc.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts" generic="Item extends string | number">
defineProps<{ items: Item[] }>()
</script>

<template>
<ul>
<li v-for="(item, idx) in items" :key="idx">{{ item }}</li>
</ul>
</template>
103 changes: 103 additions & 0 deletions test-dts/genericComponent.d-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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'

// 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: <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(GenericSfc)
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(GenericSfc)
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(GenericSfc)
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