cypress-selectors
is a library that provides a bunch of convenient declarative selectors for Cypress.
It helps to organize and re-use selectors and turns this:
const getSearchInput = () => cy.get('input');
const getSubmitSearchButton = () => cy.get('[cypress-id]=submit-search');
const getSearchResults = () => cy.get('.search-result');
const getMain = () => cy.xpath(`//div[@cypress-id='main']`)
into that:
class HomePage {
@ByType('input') searchInput: Selector;
@ByAttribute('submit-search') submitSearch: Selector;
@ByClass('search-result') searchResults: Selector;
@ByXPath(`//div[@cypress-id='main']`) main: Selector;
}
npm i -D cypress-selectors
The docs are located at https://anton-kravchenko.github.io/cypress-selectors
attribute
, class
, id
, type
, selector
, text
, link text
, name
and xpath
:import { By } from 'cypress-selectors';
import type { Selector } from 'cypress-selectors';
class Selectors {
@By.Id('main')
static main: Selector; // equivalent of - cy.get('#main')
@By.Type('input')
static input: Selector; // equivalent of - cy.get('input')
@By.Class('button')
static button: Selector; // equivalent of - cy.get('.button')
@By.Attribute('header')
static header: Selector; // equivalent of - cy.get('[cypress-id=header')
@By.Selector('ul > li .focus')
static listItem: Selector; // equivalent of - cy.get('ul > li .focus')
@By.XPath(`//input`)
static xInput: Selector; // equivalent of - cy.xpath('//input')
@By.Name('email')
static email: Selector; // equivalent of - cy.get(`[name="email"]`)
@By.Text.Exact('Foo')
static bar: Selector; // equivalent of - cy.xpath(`//*[text()='Foo']`)
@By.Text.Partial('Foo')
static p: Selector; // equivalent of - cy.xpath(`/*[contains(text(), 'Foo')]`)
@By.Link.ExactText('Link A')
static linkA: Selector; // equivalent of - cy.xpath(`//a[text()='Link A']`)
@By.Link.PartialText('Link B')
static linkB: Selector; // equivalent of - cy.xpath(`//a[contains(text(), 'Link B')]`)
}
Searching child elements
2.1 By linking parent selector via reference
class Selectors {
@ById('main') static parent: Selector;
@ByClass('button', { parent: Selectors.parent })
static children: Selector; // equivalent of - cy.get('#root .button')
}
2.2 By linking parent selector via alias
and parentAlias
attributes
class Selectors {
@ById('main', { alias: 'root' })
static parent: Selector;
@ByClass('button', { parentAlias: 'root' })
static children: Selector; // equivalent of - cy.get('#root .button')
}
Implementing Page Objects (PageObject is considered to be an anti-pattern although)
class SearchPagePO {
@ById('input') searchInput!: Selector;
@ByAttribute('submit-search') submitSearch!: Selector;
searchFor(term: string): SearchPagePO {
this.searchInput.type(term);
this.submitSearch.click();
return this;
}
}
Searching by non-default attribute (by default ByAttribute
uses cypress-id
)
class Selector {
@ByAttribute('submit', { attribute: 'cy-data' })
static customAttribute: Selector;
}
Selecting elements by index
class Selector {
@ByAttribute('row', { eq: 0 }) static firstRow: Selector;
@ByAttribute('row', { eq: 1 }) static secondRow: Selector;
}
Selecting elements by XPath
class Selector {
@ByXPath(`//div[@cypress-id='app']/div[@cypress-id='children']`) static app: Selector;
@ByXPath(`count(//div)`) static numberOfDivElements: Selector;
}
Specifying custom timeout for selectors
class Selectors {
/* Will try to find an element for up to 10 seconds */
@ById('main', { timeout: 10 * 1000 }) static parent: Selector;
/* By default, timeout for any selector is inherited from "defaultCommandTimeout" value of Cypress configuration */
@ById('app') static parent: Selector;
}
import { ResetSelectorsConfiguration, ConfigureSelectors } from 'cypress-selectors';
/* Setting configuration */
ConfigureSelectors({
defaultAttribute: 'cy-id', // Default: 'cypress-id' - sets default attribute to be used by @ByAttribute selector
isLoggingEnabled: true, // Default: false - logs generated selectors before accessing elements
searchOnlyFirstLevelDescendants: true, /* Default: false
=> if true: the lib will be using `Child Selector` for resolving `child-parent` relationship - https://api.jquery.com/child-selector/
=> if false: the lib will be using `Descendant Selector` for resolving `child-parent` relationship - https://api.jquery.com/descendant-selector/ */
});
/* Re-setting configuration to defaults */
ResetSelectorsConfiguration();
The library is built around decorators which are still a stage-2 proposal.
children-parent
linking via alias
and parentAlias
works only within a single class - if you need to link selectors from different classes use children-parent
linking via reference as shown in 2.2
.
children-parent
linking via reference uses static class fields stage-3 proposal. For some reason, babel-loader
and ts-loader
transpile code that defines static class fields differently.
For example, if you transpile the following code with babel-loader
using @babel/preset-typescript
preset and @babel/plugin-proposal-decorators, @babel/plugin-proposal-class-properties
plugins:
class Selectors {
@ById('main') static parent: Selector;
@ByClass('button', { parent: Selectors.parent }) static children: Selector;
}
you will get cannot access 'parent' before initialization
error, while if being transpiled via ts-loader
it works as expected.
However, this example could be fixed by just extracting parent
selector to a separate class as following:
class ParentSelectors {
@ById('main') static parent: Selector;
}
class ChildrenSelectors {
@ByClass('button', { parent: ParentSelectors.parent }) static children: Selector;
}
If child-parent
linking is defined this way if will work with both babel-loader
and ts-loader
.
The documentation doesn't go into details on how to set up Cypress and transpiling via ts-loader
. However, the setup of this project could be used as a good reference. The whole setup is done in 2 files: webpack.config.js
and tsconfig.json
. If you need another reference on setting up a project like this - check out this article.
All of the examples are declaring selectors as static
class fields. This is not a requirement - the same functionality could be achieved with non static
class fields. However please note, that child-parent
relationship is not going to work without parent
being declared as static
class field.