Skip to content

Commit a4e8ffa

Browse files
committed
feat(runtime-utils): improve setupState type mount + render helpers
1 parent 4516211 commit a4e8ffa

6 files changed

Lines changed: 78 additions & 19 deletions

File tree

‎examples/app-vitest-full/tests/nuxt/compatibility-mount.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ describe('mountSuspended() compatible to mount()', () => {
649649

650650
it('getCurrentComponent().proxy for compatibity for nuxt/test-utils v3.20.0', async () => {
651651
const wrapper = await mountSuspended(CompostionApi)
652-
const proxy = wrapper.getCurrentComponent().proxy as typeof wrapper.setupState
652+
const proxy = wrapper.getCurrentComponent().proxy as unknown as typeof wrapper.setupState
653653

654654
expect(proxy.disabled.value).toBe(false)
655655
expect(proxy.getValue()).toBe('')

‎src/browser/pure.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ function ensureTestIdAttribute(element: HTMLElement) {
3232
}
3333
}
3434

35-
export interface RenderResult<Props> extends LocatorSelectors {
35+
export interface RenderResult<Props, T = unknown> extends LocatorSelectors {
3636
container: HTMLElement
3737
baseElement: HTMLElement
3838
locator: Locator
3939
/**
4040
* The return value of the component setup, mocked when the `spy` option is enabled.
4141
*/
42-
setupState: SetupState
42+
setupState: SetupState<T>
4343
debug(
4444
el?: HTMLElement | HTMLElement[] | Locator | Locator[],
4545
maxLength?: number, options?:
@@ -81,7 +81,7 @@ export interface RenderResult<Props> extends LocatorSelectors {
8181
export async function render<T>(
8282
component: T,
8383
options: WrapperOptions<T> = {},
84-
): Promise<RenderResult<ComponentProps<T>>> {
84+
): Promise<RenderResult<ComponentProps<T>, T>> {
8585
const {
8686
container,
8787
baseElement,
@@ -93,7 +93,7 @@ export async function render<T>(
9393

9494
const { debug, getElementLocatorSelectors } = utils
9595

96-
const renderResult: RenderResult<ComponentProps<T>> = {
96+
const renderResult: RenderResult<ComponentProps<T>, T> = {
9797
container,
9898
baseElement,
9999
locator: page.elementLocator(container),

‎src/runtime-utils/mount.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { WrapperSuspendedOptions, WrapperSuspendedResult } from './utils/su
44

55
type WrapperFn<C> = typeof wrapperFn<C>
66
type WrapperOptions<C> = WrapperSuspendedOptions<WrapperFn<C>>
7-
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>>
7+
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>, C>
88

99
/**
1010
* `mountSuspended` allows you to mount any vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins. For example:

‎src/runtime-utils/render.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { render } from '@testing-library/vue'
55

66
type WrapperFn<C> = typeof render<C>
77
type WrapperOptions<C> = WrapperSuspendedOptions<WrapperFn<C>>
8-
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>>
8+
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>, C>
99

1010
/**
1111
* `renderSuspended` allows you to mount any vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins.

‎src/runtime-utils/utils/suspended.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { vi } from 'vitest'
22
import { Suspense, effectScope, h, nextTick, reactive, getCurrentInstance, onErrorCaptured } from 'vue'
3-
import type { App, ComponentInternalInstance, DefineComponent, SetupContext, VNode } from 'vue'
3+
import type { App, ComponentInternalInstance, DefineComponent, RenderFunction, SetupContext, VNode } from 'vue'
44
import type { RouteLocationRaw } from 'vue-router'
55
import type { ComponentMountingOptions } from '@vue/test-utils'
66

@@ -9,9 +9,20 @@ import NuxtRoot from '#build/root-component.mjs'
99

1010
import { RouterLink } from '../components/RouterLink.ts'
1111

12-
// TODO: improve return types
12+
// TODO: improve return types for sfc component's non-exposed states
13+
1314
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14-
export type SetupState = Record<string, any>
15+
type AnySetupState = Record<string, any>
16+
17+
type DefinedSetupFunction<C> = C extends { setup?: (...args: infer _) => infer R }
18+
? Exclude<Awaited<R>, void | RenderFunction>
19+
: never
20+
21+
type DefinedSetupState<C, R = DefinedSetupFunction<C>> = [R] extends [never]
22+
? AnySetupState
23+
: R
24+
25+
export type SetupState<C = unknown> = DefinedSetupState<C> & AnySetupState
1526

1627
type BaseOptions<C> = Pick<ComponentMountingOptions<C>, 'global'>
1728

@@ -59,11 +70,11 @@ export type WrapperSuspendedAdditionalOptions = {
5970

6071
export type WrapperSuspendedOptions<Fn> = WrapperFnOption<Fn> & WrapperSuspendedAdditionalOptions
6172

62-
export type WrapperSuspendedResult<Fn> = WrapperFnResult<Fn> & {
73+
export type WrapperSuspendedResult<Fn, C> = WrapperFnResult<Fn> & {
6374
/**
6475
* The return value of the component setup.
6576
*/
66-
setupState: SetupState
77+
setupState: SetupState<C>
6778
}
6879

6980
export function cleanupAll() {
@@ -115,7 +126,7 @@ export function wrapperSuspended<
115126
stubRouterLink?: boolean
116127
},
117128
): Promise<{
118-
wrapper: WrapperSuspendedResult<Fn>
129+
wrapper: WrapperSuspendedResult<Fn, C>
119130
setProps: (props: object) => void
120131
/**
121132
* Stop only the effect scopes created by this call, leaving those of other mounted components
@@ -150,7 +161,7 @@ export function wrapperSuspended<
150161

151162
let wrappedInstance: ComponentInternalInstance | null = null
152163
let setupContext: SetupContext
153-
let setupState: SetupState = {}
164+
let setupState = {}
154165

155166
const setProps = reactive<Record<string, unknown>>({})
156167

@@ -256,7 +267,7 @@ export function wrapperSuspended<
256267
if (isMountSettled) return
257268
isMountSettled = true
258269

259-
wrapper.setupState = setupState
270+
wrapper.setupState = setupState as SetupState<C>
260271

261272
resolve({
262273
wrapper,
@@ -293,7 +304,7 @@ export function wrapperSuspended<
293304
},
294305
}),
295306
},
296-
) as WrapperSuspendedResult<Fn>
307+
) as WrapperSuspendedResult<Fn, C>
297308
})
298309
}
299310

‎test/types/mount-render-helpers.test-d.ts‎

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,15 @@ const Component = defineComponent({
3232
emits: ['update:modelValue'],
3333
expose: ['method'],
3434
setup: () => {
35+
const message = ref('message')
36+
const number = ref(0)
37+
const object = ref({ a: 1, b: new Date(), c: true as boolean | undefined })
3538
return {
36-
method: () => '',
39+
message,
40+
number,
41+
object,
42+
method: () => message.value,
43+
method2: (v: number) => number.value + v,
3744
}
3845
},
3946
})
@@ -44,13 +51,39 @@ type SuspendedAdditionalOptions = {
4451
scoped?: boolean
4552
}
4653

54+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55+
type AnySetupState = Record<string, any>
56+
4757
type SuspendedAdditionalResult = {
48-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
49-
setupState: Record<string, any>
58+
setupState: {
59+
message: Ref<string>
60+
number: Ref<number>
61+
object: Ref<{ a: number, b: Date, c: boolean | undefined }>
62+
method: () => string
63+
method2: (v: number) => number
64+
} & AnySetupState
5065
}
5166

5267
type ComponentType = typeof Component
5368

69+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
70+
const AsyncSetupComponent = defineComponent({
71+
async setup() {
72+
const message = ref('message')
73+
await Promise.resolve()
74+
return {
75+
message,
76+
method: () => message.value,
77+
}
78+
},
79+
})
80+
81+
type AsyncSetupComponentType = typeof AsyncSetupComponent
82+
type AsyncSetupState = {
83+
message: Ref<string>
84+
method: () => string
85+
} & AnySetupState
86+
5487
describe('mountSuspended', () => {
5588
describe('options', () => {
5689
type Options = NonNullable<Parameters<typeof mountSuspended<ComponentType>>[1]>
@@ -88,6 +121,11 @@ describe('mountSuspended', () => {
88121
expectTypeOf<Pick<Result, keyof VueWrapper>>()
89122
.toEqualTypeOf<Pick<VueWrapper, keyof VueWrapper>>()
90123
})
124+
125+
it('has async setup setupState', () => {
126+
type AsyncResult = Awaited<ReturnType<typeof mountSuspended<AsyncSetupComponentType>>>
127+
expectTypeOf<AsyncResult['setupState']>().toEqualTypeOf<AsyncSetupState>()
128+
})
91129
})
92130
})
93131

@@ -129,6 +167,11 @@ describe('renderSuspended', () => {
129167
expectTypeOf<Pick<Result, keyof RenderResult>>()
130168
.toEqualTypeOf<Pick<RenderResult, keyof RenderResult>>()
131169
})
170+
171+
it('has async setup setupState', () => {
172+
type AsyncResult = Awaited<ReturnType<typeof renderSuspended<AsyncSetupComponentType>>>
173+
expectTypeOf<AsyncResult['setupState']>().toEqualTypeOf<AsyncSetupState>()
174+
})
132175
})
133176
})
134177

@@ -179,6 +222,11 @@ describe('browser.render', () => {
179222
expectTypeOf<Pick<Result, keyof LocatorSelectors>>()
180223
.toEqualTypeOf<Pick<LocatorSelectors, keyof LocatorSelectors>>()
181224
})
225+
226+
it('has async setup setupState', () => {
227+
type AsyncResult = Awaited<ReturnType<typeof render<AsyncSetupComponentType>>>
228+
expectTypeOf<AsyncResult['setupState']>().toEqualTypeOf<AsyncSetupState>()
229+
})
182230
})
183231

184232
describe('browser.page', () => {

0 commit comments

Comments
 (0)