cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.7k stars 3.16k forks source link

Unable to overwrite command with TS overloads #19564

Open jaroslav-kubicek opened 2 years ago

jaroslav-kubicek commented 2 years ago

Current behavior

Given I override visit command as below:

Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
  // do something with URL as string
  const char = url.includes('?')

  return originalFn(url, options);
});

I'm unable to type this:

Cypress.Commands.overwrite<'visit'>('visit', (originalFn, url, options?: Partial<VisitOptions>) => {
    // do something with URL as string
  const char = url.includes('?') // TS2339: Property 'includes' does not exist on type 'Partial  & { url: string; }'

  return originalFn(url, options); // TS2554: Expected 1 arguments, but got 2.
});

... as cy.visit is defined with two type overloads, but only the latter one is taken into account:

    visit(url: string, options?: Partial<VisitOptions>): Chainable<AUTWindow>
    visit(options: Partial<VisitOptions> & { url: string }): Chainable<AUTWindow>

Desired behavior

I'm able to override visit command and use visit(url: string, options?: Partial<VisitOptions>): Chainable<AUTWindow> variant.

Test code to reproduce

Cypress.Commands.overwrite<'visit'>('visit', (originalFn, url, options?: Partial<VisitOptions>) => {
    // do something with URL as string
  const char = url.includes('?') // TS2339: Property 'includes' does not exist on type 'Partial  & { url: string; }'

  return originalFn(url, options); // TS2554: Expected 1 arguments, but got 2.
});

Cypress Version

9.2.0

Other

No response

fugigoose commented 2 years ago

I just ran into this today as well, had to simply @ts-ignore for now. I wonder if it's related to how Parameters (used as the args type for CommandFnWithOriginalFn) is returning the following intersection:

[string, (Partial<VisitOptions> | undefined)] & [(Partial<VisitOptions> & {url: string})
mjhenkes commented 2 years ago

@jaroslav-kubicek, thanks for logging the issue and providing the reproducible example. Based on your provided code i was able to reproduce this issue locally.

alexniculae commented 2 years ago

Same in my case, but for adding a custom (dual) command with multiple definitions, not for overwriting an existing one - error and details copied below.

I also added a // @ts-ignore for Cypress.Commands.add and it's now ignoring the issue; but I think it's safer to stay on 8.3.0, as it seems to be the last stable version of Cypress.

Great features added in version(s) 9.x, but too many issues and breaking changes that were not considered or not documented; looking forward for a reliable release 🙂

(property) Cypress.Cypress.Commands: {
    add<T extends keyof Cypress.Chainable<any>>(name: T, fn: Cypress.CommandFn<T>): void;
    add<T extends keyof Cypress.Chainable<any>>(name: T, options: Cypress.CommandOptions & {
        ...;
    }, fn: Cypress.CommandFn<...>): void;
    add<T extends keyof Cypress.Chainable<...>, S extends keyof Cypress.PrevSubjectMap<...>>(name: T, options: Cypress.CommandOptions & {
        ...;
    }, fn: Cypress.CommandFnWithSubject<...>): void;
    add<T extends keyof Cypress.Chainable<...>, S extends keyof Cypress.PrevSubjectMap<...>>(name: T, options: Cypress.CommandOptions & {
        ...;
    }, fn: Cypress.CommandFnWithSubject<...>): void;
    overwrite<T extends keyof Cypress.Chainable<...>>(name: T, fn: Cypress.CommandFnWithOriginalFn<...>): void;
    overwrite<T extends keyof Cypress.Chainable<...>, S extends keyof Cypress.PrevSubjectMap<...>>(name: T, fn: Cypress.CommandFnWithOriginalFnAndSubject<...>): void;
}
@see — https://on.cypress.io/api/commands

No overload matches this call.
  Overload 1 of 4, '(name: "testCustomCommandCy9.2", options: CommandOptions & { prevSubject: false; }, fn: CommandFn<"testCustomCommandCy9.2">): void', gave the following error.
    Type 'string' is not assignable to type '(boolean | keyof PrevSubjectMap<unknown> | (keyof PrevSubjectMap<unknown>)[]) & false'.
  Overload 2 of 4, '(name: "testCustomCommandCy9.2", options: CommandOptions & { prevSubject: true | "optional" | ["optional"]; }, fn: CommandFnWithSubject<"testCustomCommandCy9.2", unknown>): void', gave the following error.
    Argument of type '(this: Context, $prevSubject: unknown, selector: string, content: Parameters<ChainableMethods<any>[T]>[1], options?: Parameters<ChainableMethods<any>[T]>[2]) => Chainable<...>' is not assignable to parameter of type 'CommandFnWithSubject<"testCustomCommandCy9.2", unknown>'.
  Overload 3 of 4, '(name: "testCustomCommandCy9.2", options: CommandOptions & { prevSubject: (keyof PrevSubjectMap<unknown>)[]; }, fn: CommandFnWithSubject<"testCustomCommandCy9.2", void | Document | JQuery<...> | Window>): void', gave the following error.
    Type '"optional"' is not assignable to type '(boolean | keyof PrevSubjectMap<unknown> | (keyof PrevSubjectMap<unknown>)[]) & (keyof PrevSubjectMap<unknown>)[]'.ts(2769)
cypress.d.ts(22, 5): The expected type comes from property 'prevSubject' which is declared here on type 'CommandOptions & { prevSubject: false; }'
cypress.d.ts(22, 5): The expected type comes from property 'prevSubject' which is declared here on type 'CommandOptions & { prevSubject: (keyof PrevSubjectMap<unknown>)[]; }'
edufarre commented 2 years ago

Do we have any updates on this? I am using the version 9.5.0 and I am still having this issue when trying to Cypress.Commands.overwrite the original visit command

dclowd9901 commented 2 years ago

One workaround for this that we used until the Cypress devs release a fix was simply swapping the visit function types in our own type definition file:

declare namespace Cypress {
  interface Chainable {
    ...
    visit(
      options: Partial<Cypress.VisitOptions & PopulatePreloadsOptions> & { url: string }
    ): Cypress.Chainable<Cypress.AUTWindow>;
    visit(
      url: string,
      options?: Partial<Cypress.VisitOptions & PopulatePreloadsOptions>
    ): Cypress.Chainable<Cypress.AUTWindow>;
    ...
  }
}

It's janky, but at least we still get type assertions.

Does anyone know what the canonical way to resolve something like this in Typescript? It seems like it's probably not solvable using the string literal generic mechanism for overwrite. Maybe a predicate?

Edit: Looks like it goes back to this question: https://github.com/microsoft/TypeScript/issues/47540

nikepol commented 1 year ago

This one helped me to fix TS2554: Expected 1 arguments, but got 2. YourCommand(value: string, any?: string, any2?:string): Chainable<JQuery<HTMLElement>>

cypress-app-bot commented 1 year ago

This issue has not had any activity in 180 days. Cypress evolves quickly and the reported behavior should be tested on the latest version of Cypress to verify the behavior is still occurring. It will be closed in 14 days if no updates are provided.

cypress-app-bot commented 12 months ago

This issue has been closed due to inactivity.

unikitty37 commented 5 months ago

…and yet, the issue persists and is still present in Cypress 13.7.3.

The auto-generated support/commands.ts has this example

// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })

but if I add this to the file:

Cypress.Commands.add('login', (email, password) => { return true })

I get

Argument of type '"login"' is not assignable to parameter of type 'keyof Chainable<any>'.

Surely if Cypress is auto-generating a .ts file, the examples it gives should have valid types?

tfrijsewijk commented 3 months ago

Still a problem in 13.10.0:

image

It's a cleaned up copy/paste from the Cypress documentation: https://docs.cypress.io/api/cypress-api/custom-commands#Overwrite-visit-command