cypress: Adding custom commands with typescript definitions leads to error

Cypress.Commands.add('myCustomCommand', '...')

Using TypeScript now results in…

Property ‘myCustomCommand’ does not exist on type ‘Chainable’)

How do we fix this @NicholasBoll @bahmutov

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 33
  • Comments: 47 (24 by maintainers)

Commits related to this issue

Most upvoted comments

I was looking at the examples preprocessors__typescript-webpack and preprocessors__typescript-browserify. TypeScript will not allow you to extend a namespace if the file is a module (has import/export/require). To get around this in the example, the command.ts file(s) cannot import/export anything. They simple contain the following code:

commands.ts:

declare namespace Cypress {
  interface Chainable<Subject> {
    myCustomCommand: typeof myCustomCommand // more DRY than the following:
    // myCustomCommand(value: string): Cypress.Chainable<JQuery>
  }
}

function myCustomCommand(value: string): Cypress.Chainable<JQuery> {
  return cy.get('foo')
}

Cypress.Commands.add('myCustomCommand', myCustomCommand)

Some other file:

cy.myCustomCommand('foo')

And the index.ts simply imports it: import './command'. I’ll also note the tsconfig.json needs to include all files in support for this to work (it looks like it does).

@shcallaway is exactly right. Chainable interfaces have to be extended:

declare namespace Cypress {
  interface Chainable<Subject> {
    myCustomCommand(value: string): Chainable<Subject>
  }
}

Cypress.Commands.add('myCustomCommand', { prevSubject: true}, snapshot)

Later:

cy.get('foo').myCustomCommand('bar')

I fixed it by adding index.d.ts file in my commands folder. In this file I added something like this:

import { MyCustomType } from '../Types';

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      login(): Chainable<MyCustomType>;
    }
  }
}

If You don’t import or export anything, just omit global namespace declaration:

declare namespace Cypress {
  interface Chainable<Subject = any> {
    login(): Chainable<MyCustomType>;
  }
}

Keep in mind that it won’t work with Typesciprt < 2.3, becuase default generics type has to be supported.

I had issues declaring the namespace when importing other files into commands.ts. I ran across this solution which worked for me:

https://github.com/cypress-io/add-cypress-custom-command-in-typescript/issues/2#issuecomment-389870033

Just wanted to help anyone else find that if they end up on this thread.

Any update with the version 2.x of cypress ? I have the same issue:

Property login does not exist on type Chainable<undefined>

I am using cypress 2.1.0 and ts 2.7.1

Is https://github.com/bahmutov/add-typescript-to-cypress still necessary ? Ts seems to work without webpack preprocessor …

For future readers, this is the TypeScript concept that makes it possible to re-declare the Cypress namespace: declaration merging.

Also, @NicholasBoll , regarding your example, what is the purpose of adding the generic?

Also I strongly encourage people use regular functions instead of custom commands: https://medium.com/@NicholasBoll/cypress-io-scaling-e2e-testing-with-custom-commands-6b72b902aab

A few days ago, I made the migration of e2e javascript tests from cypress to typescript.
All is not completely finished because the return type on each command is any. To do so, I just have to import the desired return types for each order.

command.d.ts :

import {IUserLogin} from '../../src/app/shared/services/users.service';

declare namespace Cypress {
  interface Chainable<Subject = any> {
    // login.commands
    login (username: string, password: string, shouldSuccess?: boolean): Chainable<IUserLogin>;

...

Note that at the very beginning of the file

./cypress/plugins/index.ts

, I added this magic line:

/// <reference types = "node" />

which allows me to recognize the type definitions for node and thus, to have a typescript file.

Here are the changes to supportFile and pluginsFile to do :

cypress.json

{
  "baseUrl": "http://localhost:4200",
  "video": false,
  "pluginsFile": "cypress/plugins/index.ts",
  "supportFile": "cypress/support/index.ts"
}

Here are the versions of the dependencies I use :

package.json

  "devDependencies": {
...
    "@bahmutov/add-typescript-to-cypress": "2.0.0",
    "@types/cypress": "1.1.3",
    "@types/node": "10.0.4",
    "cypress": "3.1.3",
    "ts-node": "6.1.2",
    "typescript": "2.9.2"
...
  },

If it interests people, I share a link to the cypress directory of my project :

https://gitlab.com/linagora/petals-cockpit/tree/master/frontend/cypress

Hmm I’m still getting errors (e.g. Property 'goToPage' does not exist on type 'Chainable'.). I don’t think declaration merging is working — possibly because of these two lines in cypress/index.d.ts?

// global variables added by Cypress when it runs
declare const cy: Cypress.Chainable;
declare const Cypress: Cypress.Cypress;

Here is my cypress/support/commands.ts:

declare namespace Cypress {
  interface Chainable {
    goToPage: typeof goToPage;
    login: typeof login;
    getGridHeader: typeof getGridHeader;
    clearSession: typeof clearSession;
    logout: typeof logout;
    // ...
  }
}

Cypress.Commands.add("goToPage", goToPage);
Cypress.Commands.add("login", login);
Cypress.Commands.add("getGridHeader", getGridHeader);
Cypress.Commands.add("clearSession", clearSession);
Cypress.Commands.add("logout", logout);
// ...

function goToPage(path: string): void {
// ...

Here is my cypress/tsconfig.cypress.json:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs"
  },
  "include": [
    "./*/*.ts",
    // explicitly include cypress types from node module
    "../node_modules/cypress"
  ]
}

And I don’t think it matters, but here is my cypress/plugins/index.js:

const webpack = require("@cypress/webpack-preprocessor");
module.exports = (on, config) => {
  const webpackOptions = {
    resolve: {
      extensions: [".ts", ".js"]
    },
    module: {
      rules: [
        {
          test: /\.ts$/,
          loader: "ts-loader",
          options: {
            configFile: "tsconfig.cypress.json"
          }
        }
      ]
    }
  };

  on("file:preprocessor", webpack({
    webpackOptions: webpackOptions
  }));
};

Dunno if this is solved but I ran in the same problem… Following official guides doesn’t allow me pretty much for importing any custom types to cover for custom commands definition types. However this seem to kind of work.

// cypress/support/commands.ts

import { ssoPageSelectors } from './pageSelectors'
import { IAccount } from '../data/users'

const logInAs = (account: IAccount) => {
  const { username, password } = account
  cy.visit('/')

  cy.assertElementExists(ssoPageSelectors.userNameInput)
    .type(username)
    .get('body')
    .then(($body) => {
      const passwordInput = $body.find(ssoPageSelectors.passwordInput).length

      if (!passwordInput) {
        cy.get(ssoPageSelectors.loginButton).click()
      }
    })
    .assertElementExists(ssoPageSelectors.passwordInput)
    .type(password)
    .assertElementExists(ssoPageSelectors.loginButton)
    .click()
}

const assertChildrenLength = (element: string, count: number) => {
  cy.assertElementExists(element).children().should('have.length', count)
}

const assertElementExists = (element: string) => {
  cy.get(element).should('exist')
}

const assertElementNotExists = (element: string) => {
  cy.get(element).should('not.exist')
}

const commands = [
  logInAs,
  assertChildrenLength,
  assertElementExists,
  assertElementNotExists,
]

commands.forEach((command) => Cypress.Commands.add(command.name, command))

// cypress/support/types.d.ts

/* eslint-disable unused-imports/no-unused-imports */
/* eslint @typescript-eslint/no-unused-vars: 0 */
/// <reference types="cypress" />

import { IAccount } from '../data/users'

declare global {
  namespace Cypress {
    interface Chainable {
      /**
       * Login with a given type of a user account
       * @param account - type of the account
       * @example
       * import { users } from '../../data/users'
       * cy.logInAs(users.ADMIN)
       */
      logInAs(account: IAccount): Cypress.Chainable<Element>

      /**
       * Asserts the length of children elements
       * @param element - selector as a string for element
       * @param count - expected count of child elements
       * @example
       * cy.assertChildrenLength('#element', 3)
       * cy.assertChildrenLength('.class', 3)
       */
      assertChildrenLength(
        element: string,
        count: number,
      ): Cypress.Chainable<Element>

      /**
       * Asserts that given element exists in the DOM
       * @param element - selector as a string for element
       * @example
       * cy.assertElementExists('#element')
       * cy.assertElementExists('.class')
       */
      assertElementExists(element: string): Cypress.Chainable<Element>

      /**
       * Asserts that given element exists in the DOM
       * @param element - selector as a string for element
       * @example
       * cy.assertElementExists('#element')
       * cy.assertElementExists('.class')
       */
      assertElementNotExists(element: string): Cypress.Chainable<Element>
    }
  }
}

I was having this problem. TS and custom Cypress commands were working fine, but I after I added another custom command, everything broke.

I think it was b/c I was importing something into my cypress.d.ts in the wrong spot.

//** I had been importing something here

declare namespace Cypress {
  import { SalesforceGetAccountArgs } from "../cypress/support/sf";
//** Moving it to here (within the declaration) fixed things, apparently
  import { CustomerIoMessage } from "../lib/customer-io";

  export interface Chainable<Subject> {
    sfGetAccount: (
      getAccountArgs: SalesforceGetAccountArgs
    ) => Chainable<Partial<SalesforceAccount>>;
   getSentEmails: (email: string) => Chainable<Array<CustomerIoMessage>>;
  }
}

@shcallaway, I just checkout out the branch that @bahmutov provided: https://github.com/cypress-io/snapshot/pull/11/files and changed a few things to verify:

// commands.ts
// register .snapshot() command
require('../..').register()

// merge Cypress interface with new command "snapshot()"
declare namespace Cypress {
  interface Chainable {
    snapshot(): void
  }
}

declare namespace Cypress {
  interface Chainable {
    getBody: typeof getBody
  }
}

function getBody(): Cypress.Chainable {
  return cy.get('body')
}

Cypress.Commands.add('getBody', getBody)
// spec-typescript.ts
describe('@cypress/snapshot', () => {
  beforeEach(() => {
    cy.visit('https://example.cypress.io')
  })

  context('simple types', () => {
    it('works with objects', () => {
      cy.wrap({ foo: 42 }).snapshot()
    })

    it('works with numbers', () => {
      cy.wrap(42).snapshot()
    })

    it('works with strings', () => {
      cy.wrap('foo-bar').snapshot()
    })

    it('works with arrays', () => {
      cy.wrap([1, 2, 3]).snapshot()
    })

    it('should get the body tag', () => {
      cy.getBody().should('contain', 'Kitchen Sink')
    })
  })
})

That is working for me. getBody is known and can be chained.

I fixed it by adding index.d.ts file in my commands folder. In this file I added something like this:

import { MyCustomType } from '../Types';

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      login(): Chainable<MyCustomType>;
    }
  }
}

If You don’t import or export anything, just omit global namespace declaration:

declare namespace Cypress {
  interface Chainable<Subject = any> {
    login(): Chainable<MyCustomType>;
  }
}

Keep in mind that it won’t work with Typesciprt < 2.3, becuase default generics type has to be supported.

you saved my life before i have used like these and i am worries if i have a lot of data

// cypress/support/index.d.ts
declare namespace Cypress {

  interface LabelFixture {
    dashboard: string;
  }

  interface DataFixture {
    key1: string;
    key2: number;
    // Add more properties as needed
  }

  interface Chainable {
    loginCookies(user: { email: string; password: string }): Chainable<any>;
    logout(): Chainable<any>;
    setCustomViewport(): Chainable<any>;

    fixture(name: '1dashboard/label.json'): Chainable<LabelFixture>;
    fixture(name: '1dashboard/data.json'): Chainable<DataFixture>;
  }
}

so i use your code

// now i can use import 
import DataFixture from '../fixtures/1dashboard/data.fixture'  // without break the chainable

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {

thankyou 🚀

then change to

command.ts
Cypress.Commands.add("loadDashboardLabel", () => {
  return cy.fixture('1dashboard/label.json');
});
// cypress/support/index.d.ts
import DashboardData from '../fixtures/1dashboard/data.fixture'

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
       loadDashboardLabel(): Chainable<DashboardLabel>;

I fixed it by adding index.d.ts file in my commands folder. In this file I added something like this:

import { MyCustomType } from '../Types';

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      login(): Chainable<MyCustomType>;
    }
  }
}

If You don’t import or export anything, just omit global namespace declaration:

declare namespace Cypress {
  interface Chainable<Subject = any> {
    login(): Chainable<MyCustomType>;
  }
}

Keep in mind that it won’t work with Typesciprt < 2.3, becuase default generics type has to be supported.

you saved my life before i have used like these and i am worries if i have a lot of data

// cypress/support/index.d.ts
declare namespace Cypress {

  interface LabelFixture {
    dashboard: string;
  }

  interface DataFixture {
    key1: string;
    key2: number;
    // Add more properties as needed
  }

  interface Chainable {
    loginCookies(user: { email: string; password: string }): Chainable<any>;
    logout(): Chainable<any>;
    setCustomViewport(): Chainable<any>;

    fixture(name: '1dashboard/label.json'): Chainable<LabelFixture>;
    fixture(name: '1dashboard/data.json'): Chainable<DataFixture>;
  }
}

so i use your code

// now i can use import 
import DataFixture from '../fixtures/1dashboard/data.fixture'  // without break the chainable

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {

thankyou 🚀

I fixed it by adding index.d.ts file in my commands folder. In this file I added something like this:

import { MyCustomType } from '../Types';

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      login(): Chainable<MyCustomType>;
    }
  }
}

If You don’t import or export anything, just omit global namespace declaration:

declare namespace Cypress {
  interface Chainable<Subject = any> {
    login(): Chainable<MyCustomType>;
  }
}

Keep in mind that it won’t work with Typesciprt < 2.3, becuase default generics type has to be supported.

This worked, but the index and command files have to be .js files - i wrongly still had them as .ts , which gives compilation errors.

I got that same error when I used an import in commands.ts. I had to not have any import/export in that file (makes TypeScript treat as a module). Not sure why TS thinks they are unrelated in that case.

The tsconfig was a little tricky, but I think I’m doing it the same as you. My tsconfig.json looks like this:

./cypress/tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "../node_modules",
    "sourceMap": true,
    "target": "es2017",
    "strict": true,
    "moduleResolution": "node",
    "types": [
      "mocha"
    ],
    "lib": [
      "dom",
      "es2017"
    ]
  },
  "include": [
    "../node_modules",
    "**/*.ts"
  ]
}

Note the baseUrl and the include with ../node_modules.

Also make sure you don’t have @types/cypress lurking in your node_modules. I was getting duplicate and conflicting definitions. Those types are pre-1.0.

@shcallaway Note the snapshot repo is using Cypress 1.1.4 which has no generic. 1.2.0 adds the Subject generic. This shouldn’t effect any implementation, but if you are creating your own commands in TypeScript the interface declaration will change slightly:

declare namespace Cypress {
  interface Chainable<Subject> {
    getBody: typeof getBody
  }
}

// can omit return type - it is inferred - will be `Cypress.Chainable<JQuery<HTMLBodyElement>>`
function getBody() {
  return cy.get('body')
}

Also, @NicholasBoll , regarding your example, what is the purpose of adding the generic?

@shcallaway I added the Generic (should be available on Cypress 1.2.0, but not on Cypress 1.1.4) to allow they subject to be passed around so that the .then type would be known. For backwards compatibility, the Subject is defaulted to any. For example:

cy.wrap({ foo: 'bar' })
  .then(subject => {
    subject; // $ExpectType { foo: string }
  })
  .its('foo') // keyof type checked - 'foo1' would be a type error
  .then(subject => {
    subject; // $ExpectType string
  })

Without the generic, the subject would always be any

Yes, I suppose that is all you need. That should work fine. Unless the custom command returns things other than more cypress commands, it can return void.

I’ll just note there are some subtleties to TypeScript modules. TypeScript defines global declarations as a namespace and anything with an require/import/export as a module. A module has it’s own local scope. A namespace participates in a global scope. Since Cypress has no exports, it is a namespace.

You can actually mix them for UMD modules.

Example:

// module
export default function foo() { return 'foo' }

// in another file:
import foo from './foo'
// global namespace
namespace Foo {
  interface foo {
    (): string
}

declare const foo: Foo.foo

// in another file
foo()
// UMD
// global namespace
namespace Foo {
  interface foo {
    (): string
}

declare const foo: Foo.foo

export default foo
export as namespace Foo

// in another file
foo()

// also works
import foo from './foo'
foo()

Chai exports a namespace because their API is chainable and you’ll need access to that namespace in order to extend the Assertion interface. But they also export a const called chai: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/chai/index.d.ts#L1616

More info about TypeScript modules can be found here: https://www.typescriptlang.org/docs/handbook/modules.html