cypress icon indicating copy to clipboard operation
cypress copied to clipboard

mount type definitions incompatible with Vue 3.2.38 SFC using <slot>

Open JoostKersjes opened this issue 1 year ago • 10 comments

Current behavior

Upgrading from "vue": "3.2.37" to "vue": "3.2.38" has resulted in TypeScript errors appearing in my component test files, specifically when using mount with an SFC that has a <slot />.

<script lang="ts" setup>
defineProps<{
  label: string;
}>();
</script>
  
<template>
  <button type="button" :title="label" :aria-label="label">
    <slot />
  </button>
</template>
import Button from "./Button.vue";

describe("Button", () => {
  it("should render", () => {
    cy.mount(Button); // TypeScript Error ts(2769)
  });
});
src/Button.cy.ts:5:14 - error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type 'ComponentPublicInstanceConstructor<{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, ...' is not assignable to parameter of type 'ComponentOptionsWithObjectProps<__VLS_TypePropsToRuntimeProps<{ label: string; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, string[], string, Readonly<...> & { ...; }, {}>'.
      Type 'ComponentPublicInstanceConstructor<{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, ...' is not assignable to type 'ComponentOptionsBase<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>> & { [x: `on${Capitalize<string>}`]: ((...args: any[]) => any) | undefined; }, ... 8 more ..., {}>'.
        Types of property 'setup' are incompatible.
          Type '((this: void, props: Readonly<LooseRequired<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>>>>, ctx: SetupContext<...>) => void | ... 2 more ... | Promise<...>) | undefined' is not assignable to type '((this: void, props: Readonly<LooseRequired<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>> & { [x: `on${Capitalize<string>}`]: ((...args: any[]) => any) | undefined; }>>, ctx: SetupContext<...>) => void | ... 2 more ... | Promise<...>) | undefined'.
            Type '(this: void, props: Readonly<LooseRequired<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>>>>, ctx: SetupContext<...>) => void | ... 2 more ... | Promise<...>' is not assignable to type '(this: void, props: Readonly<LooseRequired<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ label: string; }>>> & { [x: `on${Capitalize<string>}`]: ((...args: any[]) => any) | undefined; }>>, ctx: SetupContext<...>) => void | ... 2 more ... | Promise<...>'.
              Types of parameters 'ctx' and 'ctx' are incompatible.
                Type 'SetupContext<string[]>' is not assignable to type 'SetupContext<{}>'.
                  Type '{}' is missing the following properties from type 'string[]': length, pop, push, concat, and 29 more.

5     cy.mount(Button); // TypeScript Error ts(2769)
               ~~~~~~

  node_modules/cypress/vue/dist/index.d.ts:82:25
    82 export declare function mount<PropsOptions extends Readonly<ComponentPropsOptions>, RawBindings, D, C extends ComputedOptions = {}, M extends Record<string, Function> = {}, E extends EmitsOptions = Record<string, any>, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, EE extends string = string>(componentOptions: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, E, Mixin, Extends, EE>, options?: CyMountOptions<ExtractPropTypes<PropsOptions> & PublicProps, D>): Cypress.Chainable;
                               ~~~~~
    The last overload is declared here.

Desired behavior

Cypress should allow me to mount a Vue SFC component with the following syntax:

import Button from "./Button.vue";

describe("Button", () => {
  it("should render", () => {
    cy.mount(Button); // Should not throw a TypeScript error
  });
});

Test code to reproduce

Fork: https://github.com/JoostKersjes/cypress-test-tiny

Specific file that contains the error: https://github.com/JoostKersjes/cypress-test-tiny/blob/vue-3.2.38-type-error/src/Button.cy.ts

Use npm run check to output the TypeScript error to the terminal.

Cypress Version

10.7.0

Node version

v18.2.0

Operating System

Kubuntu 20.04

Debug Logs

No response

Other

No response

JoostKersjes avatar Sep 01 '22 08:09 JoostKersjes

Hmm I think Vue 3.2.28 broke some of our typings. Most likely this will be an issue for Test Utils, too, which we use internally: https://github.com/vuejs/test-utils/issues

We probably need to update Test Utils to support 3.2.28, than upgrade cypress/vue to use that, too.

This should not take too long, but in the meantime, unfortunately, you'll need do cy.mount(Button as any) or ts-ignore.

lmiller1990 avatar Sep 05 '22 05:09 lmiller1990

I tried updating our types but it wasn't that simple. I am still looking into this. I wonder why the presence of <slot /> breaks things.

lmiller1990 avatar Sep 08 '22 11:09 lmiller1990

Hey team! Please add your planning poker estimate with Zenhub @amehta265 @astone123 @lmiller1990 @marktnoonan @mike-plummer @rockindahizzy @warrensplayer @ZachJW34

baus avatar Sep 09 '22 19:09 baus

For a real world example, you can clone my personal project - the branch associated w/ this PR specifically https://github.com/lmiller1990/rhythm/pull/19/files. In that PR if you do cmd+f for ts-ignore you can see where I had to ignore errors due to this problem. It's not the exact same problem (no slots) but it's in the same area.

We've got some weird hack with the types here: https://github.com/cypress-io/cypress/blob/develop/npm/vue/inline-types.ts. I thought this was a good idea initially to capture the huge type definition here https://github.com/vuejs/test-utils/blob/main/src/mount.ts#L98-L294.

I am thinking we probably just want something more simple potentially... I wonder if just make the type of component we pass as the first arg to mount to be very permissive, like any, or at least something less complex than what we currently do.

The complexity is we need to support any version of Vue 3, which changes it's types fairly regularly (not usually a problem, except for projects like ours, where we ship a pre-bundled Test Utils). I think having worst types (eg, any for the first arg of mount would be better than having broken types.

I'm interested in picking this up, unless someone else really wants it!

lmiller1990 avatar Sep 12 '22 02:09 lmiller1990

Working on this, also reproduced it here: https://github.com/lmiller1990/cypress-issue-23653, good to know it's not specific to any one repo.

It only shows if I run yarn vue-tsc --noEmit.

The error boils down to this

Type 'SetupContext<string[]>' is not assignable to type 'SetupContext<{}>'

lmiller1990 avatar Sep 19 '22 03:09 lmiller1990

I thought about this more, I'm going to explore updating Test Utils to expose a better API to let people (like Cypress, Testing Library Vue) build on top of it. Then we can get all the types "for free" without having to mess around too much, or resort to weird work arounds.

Specifically, I think MountOptions needs to have some additional generics args to let third parties add their own mounting option types. That should help us out, as well as other people using Test Utils to build things.

lmiller1990 avatar Sep 19 '22 05:09 lmiller1990

@lmiller1990 Playwright component testing, Vue Testing Library and probably a bunch of other libraries have the same issues. I wish we had a similar util type in Vue to the one in Svelte:

import type { SvelteComponent, ComponentProps } from 'svelte/types/runtime'

type Component = SvelteComponent;
type Props = ComponentProps<SvelteComponent>;

// mount function could look like:
declare function mount<Component extends SvelteComponent>(component: Component, { props: ComponentProps<Component> });

Is it an idea to create an issue at the Vue repo to solve this problem there?

sand4rt avatar Sep 19 '22 14:09 sand4rt

ComponentPublicInstance or DefineComponent would be the equivalent in Vue - the reason Test Utils has that overly complex type is to give type safety for props, events, etc etc... there's too many ways to write a Vue component. How does Svelte give type safety for events? Eg on:message?

I agree with the Playwright team on this - it is too complex to maintain. I don't think there's really an issue to open in Vue for this - there's a lot of ways to define a Vue component, and each one needs to be typed.

I will experiment in Test Utils a bit today, at least if we can keep the complexity in one repo, other maintainers (like Cypress, Playwright, who do not have the domain specific knowledge) won't have to deal with it. I don't think copy-pasting a type that the maintainers don't understand is sustainable.

lmiller1990 avatar Sep 20 '22 00:09 lmiller1990

This is proving to be quite difficult. Ideally, I'd like some way to extend Test Utils, so we don't need to mess around with a complex type. Initial attempt: https://github.com/vuejs/test-utils/pull/1779. It includes a TS Playground link.

Backup plan would just be typing the first arg to mount as any... not ideal.

lmiller1990 avatar Sep 20 '22 02:09 lmiller1990

Someone made a neat suggestion in the TypeScript Discord... https://discord.com/channels/508357248330760243/1021617359917682708

Another neat snippet that is probably close to what we want

type MountFunc<Extra> = <T>(component: Comp<T>, options?: Opts<T> & Extra) => MountResult;

const mount: MountFunc<{ someSpecificArg: boolean }> = (comp, args) => {

}

In conjunction w/ some changes here: https://github.com/vuejs/test-utils/blob/main/src/mount.ts

Edit: I think the "copy paste + modify types" is the only practical strategy, at least for now - I cannot find a good way to make the Test Utils mount interface reusable.

lmiller1990 avatar Sep 20 '22 03:09 lmiller1990