New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding custom commands with typescript definitions leads to error #1065
Comments
Good concrete example where we could use it today is https://github.com/cypress-io/snapshot User adds custom command (in require('@cypress/snapshot')() and now has commanf cy.wrap(42).snapshot()
// or
cy.wrap(42).snapshot({name: 'meaning of life'}) It would be nice to add this command to the Cypress.Commands.add('snapshot', { prevSubject: true }, snapshot) |
One way to do this would be to export the |
@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') |
So we don't have to make any changes on our end right? We just need to update the Typescript recipes to show this... @bahmutov ? |
I'll just note there are some subtleties to TypeScript modules. TypeScript defines global declarations as a You can actually mix them for 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 More info about TypeScript modules can be found here: https://www.typescriptlang.org/docs/handbook/modules.html |
I was looking at the examples
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: |
What about commands that are supposed to return a
TypeScript won't allow us to not return a value in command, and yet this is how most commands work. |
Custom commands like this should always return a value. You should return |
What about this example, then? |
It would technically work if all you're doing is enqueing more Cypress commands, but it would not work in other situations like if you return a regular Promise. So best practice would be to always return something. We'll update the examples. It's not going to hurt anything by always returning from Custom Commands. |
So in general I should return the last |
Right so this is where it gets weird which is why we've opted not to return some things from the examples. It looks good when everything is chained together but then looks really weird if they are not... All that's happening when you do So the return value is only applicable do the current custom command. What I mean is, the The work of the other commands has just been enqueued, not run yet. What happens is that it just looks weird... Cypress.Commands.add('foo', () => {
cy.get(...)
cy.get(...)
cy.visit(...)
// super weird
return cy.request(...)
}) But returning a synchronous chainer isn't actually doing anything. So you could instead just return null to indicate you're done your promise chain to make Typescript happy. Cypress.Commands.add('foo', () => {
cy.get(...)
cy.get(...)
cy.visit(...)
cy.request(...)
return null // we are done but still weird
}) Cypress.Commands.add('foo', () => {
cy.get(...)
cy.get(...)
cy.visit(...)
cy.request(...)
// this is why we don't return anything in the examples
// because it looks most familiar to the way you write your normal test code
}) |
Given that the actual return value of the custom command has no purpose (outside of the custom command itself), why would I declare the return type to begin with? I could just do this: function login(email: string, password: string): void {
cy.request(options);
});
Cypress.Commands.add('login', login); |
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. |
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? |
I tried it out with Cypress 1.1.4 type definitions and this approach of merging Cypress interface works. https://github.com/cypress-io/snapshot/pull/11/files The new method becomes available on the interface, making TS and VSCode happy |
Hmm I'm still getting errors (e.g. // global variables added by Cypress when it runs
declare const cy: Cypress.Chainable;
declare const Cypress: Cypress.Cypress; Here is my 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 {
"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 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
}));
}; |
@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 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 |
@shcallaway I knew that TypeScript merged declarations, but I didn't know they documented it. Thanks! |
@bahmutov I don't think your PR on snapshot fully demonstrates how to appropriately use custom commands with TypeScript. Specifically, it does not address:
I believe this is due to the
For this behavior to work, all custom commands should return something like |
A suggestion and I don't know how the Cypress team feels about it, but we don't use export function login(
name: string,
password?: string,
): void {
cy.log(`Logging in as ${name}`);
cy.request({
method: 'POST',
url: '/api/webconsole/login',
body: {
username: name,
password: 'logrhythm!1',
},
}).then(response => {
console.log(response);
return response;
});
} In some other file: import { login } from '../helpers/login'
describe('Login', () => {
it('should allow the user to enter credentials', () => {
login('username', 'password');
cy.title().should('contain', 'Dashboard');
});
}); |
@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. |
😕 I must be doing something wrong then. @NicholasBoll It's good that it works as expected, though. |
@shcallaway Note the snapshot repo is using Cypress 1.1.4 which has no generic. 1.2.0 adds the 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')
} |
My steps were checking out the snapshot repo, checking out the branch and running Are you trying the snapshot repo? Something to watch out for is your {
"include": [
"node_modules/cypress",
"cypress/*/*.ts"
]
} |
@NicholasBoll I'm working off my own app — not snapshot. It appears to be configured correctly though. You can see my files here. Just in case, I checked out the snapshot TypeScript branch and ran the tests (with your changes). They pass. |
Note that I have tried forcibly importing the Cypress types into my import "../../node_modules/cypress/index.d.ts"; Which results in a lot of these:
I believe these errors occur because the return type for custom command is |
I got that same error when I used an The
{
"compilerOptions": {
"baseUrl": "../node_modules",
"sourceMap": true,
"target": "es2017",
"strict": true,
"moduleResolution": "node",
"types": [
"mocha"
],
"lib": [
"dom",
"es2017"
]
},
"include": [
"../node_modules",
"**/*.ts"
]
} Note the Also make sure you don't have |
Normally TypeScript includes I just noticed your |
I just tried it from scratch in TS 2.6.2 and Cypress 1.4.1 and it seems to work, adding new commands https://github.com/cypress-io/add-cypress-custom-command-in-typescript I suspect there are other TS definitions in |
Any update with the version 2.x of cypress ?
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 .... |
I fixed it by adding 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 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 //** 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>>;
}
} |
I've just had success extending the Cypress namespace declaration by simply including a neat Would you guys be interested in adding these simple steps as an example in your docs to show users how they can extend type declarations for when they add custom commands? |
A few days ago, I made the migration of e2e javascript tests from cypress to typescript. 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
, 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 :
{
"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 :
"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 |
@christophechevalier Typescript is weird about mixing modules and namespaces: https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html Cypress uses a namespace. You can tell because you don't import |
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 |
You can provide TypeScript definitions for custom commands, even when using JavaScript in your specs
|
I had issues declaring the namespace when importing other files into cypress-io/add-cypress-custom-command-in-typescript#2 (comment) Just wanted to help anyone else find that if they end up on this thread. |
I am using parcel instead to webpack, so can't use the webpack preprocessor. Is there any way to get the types of custom commands? |
Cypress runs each of your test files in an isolated context. It doesn't matter that your app is bundled by Parcel. You can still use the webpack plugin for Cypress. You can use the browserify plugin. You can create a Parcel plugin instead. Typescript doesn't care which one you use either. |
Thanks @NicholasBoll. Didn't knew about this. It solved my issue. |
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.
|
This worked, but the index and command files have to be .js files - i wrongly still had them as .ts , which gives compilation errors. |
Using TypeScript now results in...
Property ‘myCustomCommand’ does not exist on type ‘Chainable’)
How do we fix this @NicholasBoll @bahmutov
The text was updated successfully, but these errors were encountered: