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
132 changes: 106 additions & 26 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
ComponentOptionsWithoutProps,
ExtractPropTypes,
Component,
WritableComputedOptions,
SetupContext,
RenderFunction,
ComponentPropsOptions,
AppConfig,
VNodeProps
} from 'vue'
Expand All @@ -37,8 +41,8 @@ type SlotDictionary = {
[key: string]: Slot
}

interface MountingOptions<Props> {
data?: () => Record<string, unknown>
interface MountingOptions<Props, Data = {}> {
data?: () => Data extends object ? Partial<Data> : never
props?: Props
attrs?: Record<string, unknown>
slots?: SlotDictionary & {
Expand All @@ -49,40 +53,116 @@ interface MountingOptions<Props> {
shallow?: boolean
}

// TODO improve the typings of the overloads

type ExtractComponent<T> = T extends { new (): infer PublicInstance }
? PublicInstance
: any
export type ComputedOptions = Record<
string,
((ctx?: any) => any) | WritableComputedOptions<any>
>
export type ObjectEmitsOptions = Record<
string,
((...args: any[]) => any) | null
>
export type EmitsOptions = ObjectEmitsOptions | string[]

// Functional component
export function mount<TestedComponent extends FunctionalComponent>(
export function mount<
TestedComponent extends FunctionalComponent<Props>,
Props
>(
originalComponent: TestedComponent,
options?: MountingOptions<any>
): VueWrapper<ComponentPublicInstance>
options?: MountingOptions<Props>
): VueWrapper<ComponentPublicInstance<Props>>

// Component declared with defineComponent
export function mount<TestedComponent extends ComponentPublicInstance>(
originalComponent: { new (): TestedComponent } & Component,
options?: MountingOptions<TestedComponent['$props']>
options?: MountingOptions<TestedComponent['$props'], TestedComponent['$data']>
): VueWrapper<TestedComponent>
// Component declared with { props: { ... } }
export function mount<TestedComponent extends ComponentOptionsWithObjectProps>(
originalComponent: TestedComponent,
options?: MountingOptions<ExtractPropTypes<TestedComponent['props'], false>>
): VueWrapper<ExtractComponent<TestedComponent>>
// Component declared with { props: [] }
export function mount<TestedComponent extends ComponentOptionsWithArrayProps>(
originalComponent: TestedComponent,
options?: MountingOptions<Record<string, any>>
): VueWrapper<ExtractComponent<TestedComponent>>

// Component declared with no props
export function mount<
TestedComponent extends ComponentOptionsWithoutProps,
ComponentT extends ComponentOptionsWithoutProps & {}
Props = {},
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends Record<string, Function> = {},
E extends EmitsOptions = Record<string, any>,
EE extends string = string
Copy link
Member

Choose a reason for hiding this comment

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

Same here: if we really need all the generics, it would be great to have a test showcasing why.
Also, maybe pick more explicit names for the generic types and add comments: I struggle to follow what they refer to

Copy link
Member Author

Choose a reason for hiding this comment

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

this is basically a copy&paste from defineComponent.ts

To make it simpler to maintain (bring the types from that file), I can add a comment explaining.

>(
componentOptions: ComponentOptionsWithoutProps<
Props,
RawBindings,
D,
C,
M,
E,
EE
>,
options?: MountingOptions<never, D>
): VueWrapper<
ComponentPublicInstance<Props, RawBindings, D, C, M, E, VNodeProps & Props>
>

// Component declared with { props: [] }
export function mount<
PropNames extends string,
RawBindings,
D,
C extends ComputedOptions = {},
M extends Record<string, Function> = {},
E extends EmitsOptions = Record<string, any>,
EE extends string = string,
Props extends Readonly<{ [key in PropNames]?: any }> = Readonly<
{ [key in PropNames]?: any }
>
>(
componentOptions: ComponentOptionsWithArrayProps<
PropNames,
RawBindings,
D,
C,
M,
E,
EE,
Props
>,
options?: MountingOptions<Props, D>
): VueWrapper<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>

// Component declared with { props: { ... } }
export function mount<
// the Readonly constraint allows TS to treat the type of { required: true }
// as constant instead of boolean.
PropsOptions extends Readonly<ComponentPropsOptions>,
RawBindings,
D,
C extends ComputedOptions = {},
M extends Record<string, Function> = {},
E extends EmitsOptions = Record<string, any>,
EE extends string = string
>(
originalComponent: ComponentT extends { new (): any } ? never : ComponentT,
options?: MountingOptions<never>
): VueWrapper<ExtractComponent<TestedComponent>>
componentOptions: ComponentOptionsWithObjectProps<
PropsOptions,
RawBindings,
D,
C,
M,
E,
EE
>,
options?: MountingOptions<ExtractPropTypes<PropsOptions>, D>
): VueWrapper<
ComponentPublicInstance<
ExtractPropTypes<PropsOptions>,
RawBindings,
D,
C,
M,
E,
VNodeProps & ExtractPropTypes<PropsOptions, false>
>
>

// implementation
export function mount(
originalComponent: any,
options?: MountingOptions<any>
Expand Down
91 changes: 68 additions & 23 deletions test-dts/mount.d-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ const AppWithDefine = defineComponent({
template: ''
})

// accept props
let wrapper = mount(AppWithDefine, {
props: { a: 'Hello', b: 2 }
})
// vm is properly typed
expectType<string>(wrapper.vm.a)
// accept props- vm is properly typed
expectType<string>(
mount(AppWithDefine, {
props: { a: 'Hello', b: 2 }
}).vm.a
)
Copy link
Member

Choose a reason for hiding this comment

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

nit: I think the test is more readable when splitted like it was. Same for the following changes.


// no data provided
expectError(
mount(AppWithDefine, {
data() {
return {
myVal: 1
}
}
})
)
Copy link
Member

Choose a reason for hiding this comment

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

👍 I think more tests with data would be awesome. Can we pass data to all type of components? Can we pass data that are not declared? etc. That will greatly help us maintining the lib if we have extensive coverage of the different cases.


// can receive extra props
// ideally, it should not
Expand All @@ -45,12 +56,12 @@ const AppWithProps = {
template: ''
}

// accept props
wrapper = mount(AppWithProps, {
props: { a: 'Hello' }
})
// vm is properly typed
expectType<string>(wrapper.vm.a)
// accept props - vm is properly typed
expectType<string>(
mount(AppWithProps, {
props: { a: 'Hello' }
}).vm.a
)

// can't receive extra props
expectError(
Expand All @@ -71,31 +82,65 @@ const AppWithArrayProps = {
template: ''
}

// accept props
wrapper = mount(AppWithArrayProps, {
props: { a: 'Hello' }
})
// vm is properly typed
expectType<string>(wrapper.vm.a)
// accept props - vm is properly typed
expectType<string>(
mount(AppWithArrayProps, {
props: { a: 'Hello' }
}).vm.a
)

// can receive extra props
// as they are declared as `string[]`
mount(AppWithArrayProps, {
props: { a: 'Hello', b: 2 }
})
expectType<number>(
mount(AppWithArrayProps, {
props: { a: 'Hello', b: 2 }
}).vm.b
)

// cannot receive extra props
// if they pass use object inside
expectError(
mount(
{
props: ['a']
},
Copy link
Member

Choose a reason for hiding this comment

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

I'm happy to see that it throws an error, but I don't get how this is different from the test case with AppWithArrayProps which still succeeds? Maybe add more explanation in the comment.
Ideally, shouldn't mount(AppWithArrayProps, { props: { a: 'Hello', b: 2 } }) also fail?

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically when you use mount({ props: ['a']}) is basically the same as doing defineComponent({props:['a']}), the ThisType will be inferred as the $vm`, resulting in:

mount({
  props: ['a']
})
// type
type MountTyped = {
  props: ['a'] 
} 


const Comp = {
  props: ['a']
}
//type
type CompTyped = {
  props: string[]
}

Basically using mount/defineComponent we maintain the actual tuple value, when using a variable with the options, typescript will resolve the tuple as string[]

Copy link
Member

Choose a reason for hiding this comment

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

Got it! So the previous test should also throw if the props were declared as const right?

const AppWithArrayProps = {
  props: ['a'] as const,
  template: ''
}

Copy link
Member Author

Choose a reason for hiding this comment

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

That doesn't seem to happen, if you define as const, it will use props: {} overload 🤔

{
props: {
b: 2
}
}
)
)

const AppWithoutProps = {
template: ''
}

// can't receive extra props
expectError(
(wrapper = mount(AppWithoutProps, {
mount(AppWithoutProps, {
props: { b: 'Hello' }
}))
})
)

// except if explicitly cast
mount(AppWithoutProps, {
props: { b: 'Hello' } as never
})

// Functional tests

// wrong props
expectError((props: { a: 1 }) => {}, {
props: {
a: '222'
}
})

expectType<number>(
mount((props: { a: 1 }, ctx) => {}, {
props: {
a: 22
}
}).vm.a
)
9 changes: 5 additions & 4 deletions test-dts/shallowMount.d-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ const AppWithProps = {
}

// accept props
wrapper = shallowMount(AppWithProps, {
props: { a: 'Hello' }
})
// vm is properly typed
expectType<string>(wrapper.vm.a)
expectType<string>(
shallowMount(AppWithProps, {
props: { a: 'Hello' }
}).vm.a
)

// can't receive extra props
expectError(
Expand Down