cypress-selector-shorthand v0.0.7
cypress-selector-shorthand
Tired of the boilerplate needed for writing unambiguous selectors for Cypress UI tests? Use cypress-selector-shorthand
to create navigation shorthands!
With this plugin, the following examples have full IntelliSense, and you can chain arbitrary Cypress commands off of your app selectors:
myApp.appInfo.createNewUserSection.firstNameInput
.type('First');
myApp.appInfo.createNewUserSection.lastNameInput
.type('Last');
myApp.appInfo.createNewUserSection.submitButton
.click();
myApp.appInfo.createNewUserSection
.within(({ firstNameInput, lastNameInput, submitButton }) => {
firstNameInput
.type('First');
lastNameInput
.type('Last');
submitButton
.click();
});
The above snippets are equivalent to the following raw Cypress:
cy.get('[data-test=app_info] [data-test=create_new_user_section] [data-test=first_name_input]')
.type('First');
cy.get('[data-test=app_info] [data-test=create_new_user_section] [data-test=last_name_input]')
.type('Last');
cy.get('[data-test=app_info] [data-test=create_new_user_section] [data-test=submit_button]')
.click();
cy.get('[data-test=app_info] [data-test=create_new_user_section]')
.within(() => {
cy.get('[data-test=first_name_input]')
.type('First');
cy.get('[data-test=last_name_input]')
.type('Last');
cy.get('[data-test=submit_button]')
.click();
});
There's also new cy.tget
command. It works the same way as the built-in cy.get
, but automatically wraps non-css/jQuery identifiers with [data-test]
.
For example:
cy.tget('app_info:visible create_new_user_section submit_button');
will be converted automatically into
cy.get('[data-test=app_info]:visible [data-test=create_new_user_section] [data-test=submit_button]');
Installation
pnpm add -D cypress-selector-shorthand
# or
npm i -D cypress-selector-shorthand
# or
yarn add -D cypress-selector-shorthand
Add the appropriate import to your project's cypress/support/commands.js
(or commands.ts
):
require('cypress-selector-shorthand/install');
import 'cypress-selector-shorthand/install';
Configuration
To make use of the navigation shorthands, there's three extra steps you need to do to generate the objects and interfaces. (Note: you can use cy.tget()
without this.)
First, you need to create a schema JSON file, e.g. the following might be used for the example at the top of this file:
./myAppSchema.json
{
"app_info": {
"create_new_user_section": {
"name_input": null,
"address_input": null,
"submit_button": null
},
"existing_users_table": {
"user_rows": {
"user_row": {
"firstName": null,
"lastName": null
}
}
}
}
}
Once you have your schema, you need to generate interfaces for IntelliSense:
npx cypress-selector-shorthand interfaces --appName 'MyApp' --schema './myAppSchema.json' --outfile './src/myAppInterface.ts'`
This will create a number of interfaces from your schema file (using the quicktype
library). For example, the below is the result of the sample JSON above.
./src/myAppInterface.ts
export type MyApp = {
appInfo: Cypress.ChainableLike<JQuery<HTMLElement>, AppInfo>;
} & { [key: string]: Cypress.ChainableLike<JQuery<HTMLElement>, AppInfo> };
export type AppInfo = {
createNewUserSection: Cypress.ChainableLike<JQuery<HTMLElement>, CreateNewUserSection>;
existingUsersTable: Cypress.ChainableLike<JQuery<HTMLElement>, ExistingUsersTable>;
} & { [key: string]: Cypress.ChainableLike<JQuery<HTMLElement>, CreateNewUserSection | ExistingUsersTable> };
export type CreateNewUserSection = {
nameInput: Cypress.Chainable<JQuery<HTMLElement>>;
addressInput: Cypress.Chainable<JQuery<HTMLElement>>;
submitButton: Cypress.Chainable<JQuery<HTMLElement>>;
};
export type ExistingUsersTable = {
userRows: Cypress.ChainableLike<JQuery<HTMLElement>, UserRows>;
} & { [key: string]: Cypress.ChainableLike<JQuery<HTMLElement>, UserRows> };
export type UserRows = {
userRow: Cypress.ChainableLike<JQuery<HTMLElement>, UserRow>;
} & {
with: UserWith;
} & { [key: string]: Cypress.ChainableLike<JQuery<HTMLElement>, UserRow> };
export type UserRow = {
firstName: Cypress.Chainable<JQuery<HTMLElement>>;
lastName: Cypress.Chainable<JQuery<HTMLElement>>;
};
export interface UserWith {
firstName: (text: string) => Cypress.ChainableLike<JQuery<HTMLElement>, UserRow>;
lastName: (text: string) => Cypress.ChainableLike<JQuery<HTMLElement>, UserRow>;
}
You might notice this generated a with
field that didn't exist in the schema. More on that under the Usage section.
Finally, you need to generate your navigation object:
myApp.ts
import { generateNavigationObject } from 'cypress-selector-shorthand';
import type MyApp from './myAppInterface.ts';
const myApp = generateNavigationObject<MyApp>(schema);
export { myApp };
Usage
cy.tget(selector, options)
behaves the same way as cy.get
, but it processes your selector string before passing it to cy.get
.
For any tokens in selector
that are not inside ()
/{}
/[]
and do not start with @
, #
, .
, >
, :
, [
, (
, or {
, the token is wrapped as [data-test=${token}]
. If token is followed by a :
, >
, or ~
, those are left alone. So token:visible
will become [data-test=token]:visible
, etc.
If you are using the generated selector shorthands, you can chain Cypress commands off them. The shorthand chain is not evaluated until you chain a Cypress command off of it. So myApp.appInfo.existingUsersSection
will not do anything, but myApp.appInfo.existingUsersSection.refresh.click()
is equivalent to cy.tget('my_app app_info existing_users_section refresh').click()
.
Note that the selector shorthand generation looks for fields ending with rows
/items
/menu-items
with a matching direct descendent that also has descendants (i.e. row
/item
/menu-item
), and generates a corresponding with
property. So in the example, you can use myApp.appInfo.existingUsersSection.rows.with.firstName('John')
and that will return all rows for people with the first name John. This is equivalent to cy.tget('my_app app_info existing_users_section rows row:has([data-test="firstName"]:contains(John))')
, except the tget won't actually fire until you chain further Cypress commands off of it.
The selector shorthands are also passed to .within
. If you use .within
on a regular Cypress command, it behaves exactly like normal, but if you chain .within
on a generated navigation object, it yields the navigation object AND the previous subject:
import { myApp } from './myApp.ts';
cy.tget('something')
// Normal Cypress callback parameters
.within((prevSubject) => {});
myApp.appInfo
// Parameter order is different! First argument matches whatever shorthand the `.within` was chained off of,
// whereas the second argument is generally going to be the corresponding element itself.
.within((appInfo, prevSubject) => {});
myApp.appInfo
// You can destructure and continue using shorthands:
.within(({ createNewUserSection, existingUsersTable }) => {
createNewUserSection
.should('be.visible');
existingUsersTable
.scrollIntoView();
});
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago