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
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ describe('mountSuspended() compatible to mount()', () => {

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

expect(proxy.disabled.value).toBe(false)
expect(proxy.getValue()).toBe('')
Expand Down
8 changes: 4 additions & 4 deletions src/browser/pure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ function ensureTestIdAttribute(element: HTMLElement) {
}
}

export interface RenderResult<Props> extends LocatorSelectors {
export interface RenderResult<Props, T = unknown> extends LocatorSelectors {
container: HTMLElement
baseElement: HTMLElement
locator: Locator
/**
* The return value of the component setup, mocked when the `spy` option is enabled.
*/
setupState: SetupState
setupState: SetupState<T>
debug(
el?: HTMLElement | HTMLElement[] | Locator | Locator[],
maxLength?: number, options?:
Expand Down Expand Up @@ -81,7 +81,7 @@ export interface RenderResult<Props> extends LocatorSelectors {
export async function render<T>(
component: T,
options: WrapperOptions<T> = {},
): Promise<RenderResult<ComponentProps<T>>> {
): Promise<RenderResult<ComponentProps<T>, T>> {
const {
container,
baseElement,
Expand All @@ -93,7 +93,7 @@ export async function render<T>(

const { debug, getElementLocatorSelectors } = utils

const renderResult: RenderResult<ComponentProps<T>> = {
const renderResult: RenderResult<ComponentProps<T>, T> = {
container,
baseElement,
locator: page.elementLocator(container),
Expand Down
2 changes: 1 addition & 1 deletion src/runtime-utils/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { WrapperSuspendedOptions, WrapperSuspendedResult } from './utils/su

type WrapperFn<C> = typeof wrapperFn<C>
type WrapperOptions<C> = WrapperSuspendedOptions<WrapperFn<C>>
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>>
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>, C>

/**
* `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:
Expand Down
2 changes: 1 addition & 1 deletion src/runtime-utils/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { render } from '@testing-library/vue'

type WrapperFn<C> = typeof render<C>
type WrapperOptions<C> = WrapperSuspendedOptions<WrapperFn<C>>
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>>
type WrapperResult<C> = WrapperSuspendedResult<WrapperFn<C>, C>

/**
* `renderSuspended` allows you to mount any vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins.
Expand Down
37 changes: 28 additions & 9 deletions src/runtime-utils/utils/suspended.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { vi } from 'vitest'
import { Suspense, effectScope, h, nextTick, reactive, getCurrentInstance, onErrorCaptured } from 'vue'
import type { App, ComponentInternalInstance, DefineComponent, SetupContext, VNode } from 'vue'
import type { App, ComponentInternalInstance, DefineComponent, RenderFunction, SetupContext, VNode } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type { ComponentMountingOptions } from '@vue/test-utils'

Expand All @@ -9,9 +9,28 @@ import NuxtRoot from '#build/root-component.mjs'

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

// TODO: improve return types
// TODO: improve return types for sfc component's non-exposed states

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type SetupState = Record<string, any>
type AnySetupState = Record<string, any>

type GetDefinedSetupState<
Component,
SetupReturn = Component extends { setup?: (...args: infer _) => infer R } ? Awaited<R> : never,
IsOptionalReturn = [Extract<SetupReturn, undefined>] extends [never] ? false : true,
DefinedSetupState = Exclude<SetupReturn, void | RenderFunction>,
> = IsOptionalReturn extends true
? Partial<DefinedSetupState>
: DefinedSetupState

type GetSetupState<
Component,
DefinedSetupState = GetDefinedSetupState<Component>,
> = [DefinedSetupState] extends [never]
? AnySetupState
: DefinedSetupState

export type SetupState<C = unknown> = GetSetupState<C> & AnySetupState

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

Expand Down Expand Up @@ -59,11 +78,11 @@ export type WrapperSuspendedAdditionalOptions = {

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

export type WrapperSuspendedResult<Fn> = WrapperFnResult<Fn> & {
export type WrapperSuspendedResult<Fn, C> = WrapperFnResult<Fn> & {
/**
* The return value of the component setup.
*/
setupState: SetupState
setupState: SetupState<C>
}

export function cleanupAll() {
Expand Down Expand Up @@ -115,7 +134,7 @@ export function wrapperSuspended<
stubRouterLink?: boolean
},
): Promise<{
wrapper: WrapperSuspendedResult<Fn>
wrapper: WrapperSuspendedResult<Fn, C>
setProps: (props: object) => void
/**
* Stop only the effect scopes created by this call, leaving those of other mounted components
Expand Down Expand Up @@ -150,7 +169,7 @@ export function wrapperSuspended<

let wrappedInstance: ComponentInternalInstance | null = null
let setupContext: SetupContext
let setupState: SetupState = {}
let setupState = {}

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

Expand Down Expand Up @@ -256,7 +275,7 @@ export function wrapperSuspended<
if (isMountSettled) return
isMountSettled = true

wrapper.setupState = setupState
wrapper.setupState = setupState as SetupState<C>

resolve({
wrapper,
Expand Down Expand Up @@ -293,7 +312,7 @@ export function wrapperSuspended<
},
}),
},
) as WrapperSuspendedResult<Fn>
) as WrapperSuspendedResult<Fn, C>
})
}

Expand Down
143 changes: 140 additions & 3 deletions test/types/mount-render-helpers.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ const Component = defineComponent({
emits: ['update:modelValue'],
expose: ['method'],
setup: () => {
const message = ref('message')
const number = ref(0)
const object = ref({ a: 1, b: new Date(), c: true as boolean | undefined })
return {
method: () => '',
message,
number,
object,
method: () => message.value,
method2: (v: number) => number.value + v,
}
},
})
Expand All @@ -44,9 +51,17 @@ type SuspendedAdditionalOptions = {
scoped?: boolean
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySetupState = Record<string, any>

type SuspendedAdditionalResult = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setupState: Record<string, any>
setupState: {
message: Ref<string>
number: Ref<number>
object: Ref<{ a: number, b: Date, c: boolean | undefined }>
method: () => string
method2: (v: number) => number
} & AnySetupState
}

type ComponentType = typeof Component
Expand Down Expand Up @@ -187,3 +202,125 @@ describe('browser.render', () => {
})
})
})

describe('wrapper.setupState', () => {
type WrapperSetupStates<C> = {
mountSuspended: Awaited<ReturnType<typeof mountSuspended<C>>>['setupState']
renderSuspended: Awaited<ReturnType<typeof renderSuspended<C>>>['setupState']
render: Awaited<ReturnType<typeof render<C>>>['setupState']
}

type ExpectedSetupStates<T = AnySetupState> = {
mountSuspended: T & AnySetupState
renderSuspended: T & AnySetupState
render: T & AnySetupState
}

it('when setup returns state, infers setupState from it', () => {
const setup = () => ({
message: ref(''),
method: () => '' as string,
})

type Expected = ExpectedSetupStates<ReturnType<typeof setup>>

const _c1 = defineComponent({ setup })
expectTypeOf<WrapperSetupStates<typeof _c1>>().toEqualTypeOf<Expected>()

const _c2 = defineComponent({ setup: async () => setup() })
expectTypeOf<WrapperSetupStates<typeof _c2>>().toEqualTypeOf<Expected>()
})

it('when setup returns void, falls back to any setupState', () => {
const setup = (): void => {}

type Expected = ExpectedSetupStates

const _c1 = defineComponent({ setup })
expectTypeOf<WrapperSetupStates<typeof _c1>>().toEqualTypeOf<Expected>()

const _c2 = defineComponent({ setup: async () => setup() })
expectTypeOf<WrapperSetupStates<typeof _c2>>().toEqualTypeOf<Expected>()
})

it('when setup returns undefined, falls back to any setupState', () => {
const setup = (): undefined => {}

type Expected = ExpectedSetupStates

const _c1 = defineComponent({ setup })
expectTypeOf<WrapperSetupStates<typeof _c1>>().toEqualTypeOf<Expected>()

const _c2 = defineComponent({ setup: async () => setup() })
expectTypeOf<WrapperSetupStates<typeof _c2>>().toEqualTypeOf<Expected>()
})

it('when setup returns a union of states, keeps the union in setupState', async () => {
const setup = (props: object) => {
const method = () => '' as string
return props
? {
tag: '1' as const,
message: ref(''),
method,
cMethod: () => new Date(),
}
: {
tag: '2' as const,
method,
cMethod: () => 1 as number,
}
}

type Expected = ExpectedSetupStates<ReturnType<typeof setup>>

const _c1 = defineComponent({ setup })
expectTypeOf<WrapperSetupStates<typeof _c1>>().toEqualTypeOf<Expected>()

const _c2 = defineComponent({ setup: async props => setup(props) })
expectTypeOf<WrapperSetupStates<typeof _c2>>().toEqualTypeOf<Expected>()
})

it('when setup may return undefined, makes setupState partial', async () => {
const setup = (props: unknown) => {
if (!props) return
return {
message: ref(''),
method: () => '' as string,
}
}

type Expected = ExpectedSetupStates<{
message?: Ref<string>
method?: () => string
}>

const _c1 = defineComponent({ setup })
expectTypeOf<WrapperSetupStates<typeof _c1>>().toEqualTypeOf<Expected>()

const _c2 = defineComponent({ setup: async props => setup(props) })
expectTypeOf<WrapperSetupStates<typeof _c2>>().toEqualTypeOf<Expected>()
})

it('when setup returns a render function, falls back to any setupState', () => {
const setup = () => {
const message = ref('hello')
return () => h('div', message.value)
}

type Expected = ExpectedSetupStates

expectTypeOf<WrapperSetupStates<typeof setup>>().toEqualTypeOf<Expected>()

const _c1 = defineComponent(setup)
expectTypeOf<WrapperSetupStates<typeof _c1>>().toEqualTypeOf<Expected>()

const _c2 = defineComponent({ setup })
expectTypeOf<WrapperSetupStates<typeof _c2>>().toEqualTypeOf<Expected>()
})

it('when component has no setup, falls back to any setupState', () => {
const _c = defineComponent({ render: () => h('div') })
expectTypeOf<WrapperSetupStates<typeof _c>>().toEqualTypeOf<ExpectedSetupStates>()
})
})
Loading