cypress-io / cypress-documentation

Cypress Documentation including Guides, API, Plugins, Examples, & FAQ.
https://docs.cypress.io
MIT License
937 stars 1.04k forks source link

Cypress Custom Command typings need better documentation #2565

Open dudewad opened 4 years ago

dudewad commented 4 years ago

Current behavior:

I am completely unable to get cypress typings to register for commands created using Cypress.Commands.add. I've got a repo that has several e2e projects within it that share a set of testing code (this shared lib lives at ./tools/testing). ./tools/testing/e2e/support/commands.ts is where we run several Cypress.Commands.add calls.

Alongside that file I placed a global.d.ts file (I've also moved it around and tried naming it index.d.ts, etc). Running tsc -p tsconfig.json --listFiles shows that the global.d.ts is included, yet it appears to have no effect. That file contains the following:

declare namespace Cypress {
  interface Chainable<Subject> {
    getIframeWindow(selector: string): Chainable<Subject>;
    getIframeBody(selector: string): Chainable<Subject>;
    replaceIFrameFetchWithXhr(selector: string): Chainable<Subject>;
  }
}

This is following this discussion (and many others that I've found) here

I've also tried wrapping the namespace declaration in a global but that doesn't seem to help, either.

It's clear that tsc has included my global file but I can't discern if this is a Cypress issue or a Typescript issue.

The above commands that are created using Cypress example recipes. However, given linting, I can't access them without using array notation or tsc will choke on them at dev time. Using dot notation to access them then throws the error:

TS2339: Property 'getIframeWindow' does not exist on type 'cy & EventEmitter'.

Desired behavior:

Improve documentation on how to add typings. It doesn't seem realistic that there's a whole page on how to create these commands but no expectation that people will not want typings to be supported for them.

Test code to reproduce

As this is a typings issue not a test issue I'm going to add the reproduction inline here.

Add a command:

Cypress.Commands.add('getIframeWindow', (iFrameSelector = '') => {
  cy.log(`getIframeWindow iframe${iFrameSelector}`);

  return cy
    .get(`iframe${iFrameSelector}`, { log: false })
    // @ts-ignore
    .its('0.contentWindow', { log: false })
    .should('exist', { log: false })
    .then(window => cy.wrap(window, { log: false }));
});

Type the command:

declare namespace Cypress {
  interface Chainable<Subject> {
    getIframeWindow(selector: string): Chainable<Subject>;
  }
}

Versions

Cypress 3.8.2 TS 3.4.5

jennifer-shehane commented 4 years ago

This repo is only for bug reports and feature requests relating to the Cypress product. If you want to open an issue on our documentation, you can open it here.

This issue will be transferred to our cypress-documentation repo.

jennifer-shehane commented 4 years ago

cc @bahmutov

NitradoJustus commented 4 years ago

Having the same issue, tests do work, but the IntelliJ won't recognize the typings (custom commands are red undersquiggled). Something is still amiss but I'm wasting too much time trying to find out :( Tried everything in https://github.com/cypress-io/cypress/issues/1065 I guess it has to do with a conflicting monorepo setup :| Maybe better docs could help me too.

jennifer-shehane commented 4 years ago

cc @CypressJoseph 😅

shwarcu commented 4 years ago

@bahmutov @jennifer-shehane @CypressJoseph it's impossible to get types working when following the official guide given here https://docs.cypress.io/api/cypress-api/custom-commands.html#5-Write-TypeScript-definitions showcase

Could you please check this topic?

jamie--stewart commented 4 years ago

Hi folks, this issue comment helped me work around the issue: https://github.com/cypress-io/add-cypress-custom-command-in-typescript/issues/2#issuecomment-389870033

Fix seems to be declaring Cypress in the global namespace, and your custom command definitions in there (copied from ☝️ ):

declare global {
  namespace Cypress {
    interface Chainable {
      customCommand: typeof customCommand;
    }
  }
}

function customCommand(input: MyCustomClass) {
  // ...
}

Cypress.Commands.add('customCommand', customCommand);

But agree that the solution suggested in the docs doesn't work

CypressCecelia commented 4 years ago

In the RWA the types are declared the same way as in the docs. I'm thinking this may be related to the config. Will continue to investigate.

https://github.com/cypress-io/cypress-realworld-app/blob/develop/cypress/global.d.ts

jennifer-shehane commented 4 years ago

Also see: https://github.com/cypress-io/cypress-example-recipes/pull/540

shwarcu commented 4 years ago

Hi @jennifer-shehane, is there any progress on this topic? It has been reported months ago and backed up with examples.

vincentpalita commented 3 years ago

I finally got my commands working in a lib like you have @dudewad The trick was to have the following line at the top of the file: /// <reference types="cypress" />

combined with @jamie--stewart solution (https://github.com/cypress-io/cypress-documentation/issues/2565#issuecomment-651251129)

So my file looks like this:

/// <reference types="cypress" />

declare global {
  namespace Cypress {
    interface Chainable {
      customCommand: typeof customCommand;
    }
  }
}

function customCommand(input: MyCustomClass) {
  // ...
}

Cypress.Commands.add('customCommand', customCommand);

With that everything works fine. Typings are ok when running Cypress, and I finally get completion for my added commands in my project.

tkontis commented 3 years ago

@jamie--stewart thank you for your global namespace suggestion! That was what I was missing in my case. Once I added it, all the commands that were highlighted as syntax errors were automatically resolved.

Also, to add my 2 cents to the whole thread, I found out that other plugins like cypress-axe that also augment the cy Chainable interface, had to be also referenced via triple slash statement in the same place where the main cypress types were also referenced.

So for example, in my commands.ts I have at the top:

/// <reference types="cypress" />
/// <reference types="@types/cypress-axe" />

declare global {
  namespace Cypress {
    interface Chainable<Subject> {
      register(email: string, password: string): Cypress.Chainable<null>;
    }
  }
}

function register(email: string, password: string) {
  // implementation
}

Cypress.Commands.add('register', register);

Strangely enough, despite the fact that I am using applitools as well, their typings are automatically resolved without the need to include a respective triple slash reference.

stoplion commented 3 years ago

Following the docs here: https://docs.cypress.io/api/cypress-api/custom-commands.html#See-also Doesn't work.

Getting a Property 'customAction' does not exist on type 'cy & EventEmitter' None of the suggestions here have worked.

Going to switch back to JS 🤷🏻‍♂️

jennifer-shehane commented 3 years ago

We're open to pull requests to improve this doc. There are examples of definitions of custom commands in both https://github.com/cypress-io/cypress-realworld-app/blob/develop/cypress/global.d.ts and https://github.com/cypress-io/cypress-example-recipes/pull/540

toniton commented 3 years ago

Had a similar issue and was able to fix it by placing the custom index.d.ts file in the root directory in types folder of the cypress project and not in the support folder.

File: index.d.ts 💊

// in cypress/index.d.ts
// load type definitions that come with Cypress module
/// <reference types="cypress" />
declare namespace Cypress {
    interface Chainable {
        dataCy(value: string): Chainable<any>;
    };
}

Worked

.cypress/
+---- support/
|     +---- commands.ts
.types/
+---- index.d.ts

Didn't work

.cypress/
+---- support/
|     +---- commands.ts
|     +---- index.d.ts

TS Config 🚗

{
    "include": ["./**/*.ts", "types/**/*.d.ts"]
}
dominikabieder commented 3 years ago

I still have this problem, that all my custom commands result with TS2339: Property 'When' does not exist on type 'cy & EventEmitter'.

My tscondif.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "baseUrl": "src",
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": false,
    "noEmit": true,
    "jsx": "react",
    "types": [
      "node",
      "jest",
      "@testing-library/jest-dom",
      "cypress",
      "cypress-file-upload",
      "@testing-library/cypress"
    ]
  },
  "include": [
    "src/**/*",
    "../node_modules/cypress",
    "test/cypress/acceptance/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}

in commands.ts

/// <reference types="cypress" />

require('@testing-library/cypress/add-commands');
require('cypress-file-upload');

// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
  interface Chainable {
    /**
     * Logs a BDD Given statement
     * @example
     * cy.given('I open the user list')
     */
    Given(title: string): Chainable<any>;

    /**
     * Logs a BDD When statement
     * @example
     * cy.when('I create a user')
     */
    When(title: string): Chainable<any>;

    /**
     * Logs a BDD Then statement
     * @example
     * cy.then('I see a user')
     */
    Then(title: string): Chainable<any>;

    /**
     * Logs a BDD And statement
     * @example
     * cy.and('I see an updated user count')
     */
    And(title: string): Chainable<any>;

    /**
     * Logs a BDD Finally statement
     * @example
     * cy.finally('I delete the user again')
     */
    Finally(title: string): Chainable<any>;

    /**
     * Logs in
     * @example
     * cy.login()
     */
    login(): Chainable<any>;
  }
}

Cypress.Commands.add('Given', (message) => cy.log(`**GIVEN ${message}**`));
Cypress.Commands.add('When', (message) => cy.log(`**WHEN ${message}**`));
Cypress.Commands.add('Then', (message) => cy.log(`**THEN ${message}**`));
Cypress.Commands.add('And', (message) => cy.log(`**AND ${message}**`));
Cypress.Commands.add('Finally', (message) => cy.log(`**FINALLY ${message}**`));

Cypress.Commands.add('login', () => {
  cy.visit('/login');
  cy.findByPlaceholderText(/e-mail/i).type(Cypress.env('EMAIL'));
  cy.findByPlaceholderText(/password/i).type(Cypress.env('PASSWORD'));
  cy.findByRole('button', { name: /ready/i }).click();
});

also, this setup made my unit tests broken, I've added jest to typesc in tsconfig, but it now requires typescript on unit tests, which is what I don't want 👎 the setup is really confusing and error prone.

lukepearson commented 3 years ago

The following configuration works for me:

Typescript Version 4.2.3 Cypress package version: 5.6.0 Cypress binary version: 5.6.0

web_app/cypress/support/commands.ts:

import { tag } from './tag';

declare global {
  // eslint-disable-next-line no-redeclare
  namespace Cypress {
    interface Chainable {
      tag: typeof tag;
    }
  }
}

Cypress.Commands.add('tag', tag);

web_app/cypress/.eslintrc:

{
  "plugins": [
    "cypress"
  ],
  "env": {
    "cypress/globals": true
  },
  "extends": [
    "plugin:cypress/recommended"
  ],
  "globals": {
    "tag": "readonly"
  }
}

web_app/cypress/tsconfig.json:

{
  "extends": "../tsconfig.json",
  "include": ["./**/*.ts"],
  "exclude": [],
  "compilerOptions": {
    "types": ["cypress"],
    "lib": ["esnext", "dom.iterable", "dom"],
    "isolatedModules": false,
    "allowJs": true,
    "noEmit": true
  }
}

TypeScript picks up the function signature from the imported file:

typeHint
elevatebart commented 3 years ago

Thank you @shwarcu,

There is a change you have to do to your example for the types to work. You are missing the extension of the declaration file you triple slash reference.

It is counter intuitive (and seriously backwards) but triple slash comments are not exactly node related. They do not use node commonjs resolution. So if you omit the extension, the resolution are simply ignored.

The good news is there is a better solution: merge the script and the type declaration. Look at this example repo https://github.com/cypress-io/add-cypress-custom-command-in-typescript.

I will make a PR to the docs to explain that better.

I hope it helps

[EDIT] I made the PR

PiotrFidurski commented 2 years ago

Thanks @toniton this helped in my case.

hematy61 commented 2 years ago

none of the above suggestions worked for me, though what worked for me was /// <reference path="../../../cypress/global.d.ts" /> in one of my spec files. I'm using create-react-app The problem is that create-react-app by default has turned isolatedModules to true in tsconfig.json file. Therefore, without this line, typescript considers global.d.ts as an isolated module and won't consider its content. However with this line, Typescript has a reference to global.d.ts and then will consider it as a module.