cypress-io / add-cypress-custom-command-in-typescript

Testing how new Cypress commands are added in TypeScript

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Custom commands that pass prevSubject don't work with TypeScript

shcallaway opened this issue · comments

The following code produces a compilation error: "Expected 1 arguments, but got 0."

// support/commands.ts

function doSomething(subject) {
  console.log(subject);
}

Cypress.Commands.add(
  "doSomething",
  {
    prevSubject: true
  },
  doSomething
);

declare namespace Cypress {
  interface Chainable {
    doSomething: typeof doSomething;
  }
}
// integration/test.ts

it('works', () => {
  cy.get('body').doSomething();
});

When you use typeof, Typescript declares the method using exactly the same interface of itself (in this case, (subject: any) => void).

When you call the method, you pass one less argument and Typescript understands this as a wrong call.

What you need to do is adapt the type of the method in the interface you are declaring, in your case, like this:

declare namespace Cypress {
  interface Chainable {
    doSomething: () => void;
  }
}

Another example:

function doSomethingWithArgs(subject, arg1, arg2) {
  console.log(subject, arg1, arg2);
  return true;
}

Cypress.Commands.add(
  "doSomethingWithArgs",
  {
    prevSubject: true
  },
  doSomethingWithArgs
);

declare namespace Cypress {
  interface Chainable {
    doSomethingWithArgs: (arg1: any, arg2: any) => boolean;
}

That will work!

If you have complex types for your args that you don't want to re-type inside the declaration you can do something like this:

type ChainableCommand<T extends (...args: any) => any>
  = T extends (subject: infer I, ...args: infer P) => infer R
    ? (...args: P) => R
    : never;

declare namespace Cypress {
  interface Chainable<Subject> {
    doSomethingWithArgs: ChainableCommand<typeof doSomethingWithArgs>;
  }
}

Cypress.Commands.add('doSomethingWithArgs', { prevSubject: true }, doSomethingWithArgs);

function doSomethingWithArgs(subject: Cypress.Chainable, other: string, args: number) {
  console.log(subject, other, args);
  return subject;
}

Explanation: ChainableCommand maps the type of a function A into the type of a function B that takes the same parameters as A excluding the first one and returns the same thing as A.

The solutions proposed here work fine with Cypress up to version 8.x but stopped working in Cypress 9. Any idea on how to make it work with the newest version of Cypress without having to resort to brutally overriding the type system by doing the following?

Cypress.Commands.add('myShinyNewCommand', { prevSubject: true }, (myCmd  as unknown) as MyTypeWithoutSubject);