cypress-selene
v1.0.0-alpha.4
Published
Selene and Selenide based API to Cypress for easy to write and support Web UI tests
Downloads
11
Readme
Summary
This library is a bunch of cypress extensions for writing more «user-oriented» and «easier to use» High-Level System End-to-End tests. It often uses ideas of the Selenides family of Web UI Testing Frameworks (like Selenide in Java, Selene in Python, NSelene in C#, SelenideJs in JavaScript).
See changelog for detailed feature break down;)
Table of contents
- Summary
- Table of contents
- Why is it needed?
- Intallation
- Disclaimer
- Main features breakdown
- Differences from other testing libraries
Why is it needed?
Because raw Cypress:
- lacks important "colellection conditions" aka "matcher for collections"
- retries only the last command, that leads to longer selectors, that can't be break down for faster support on failure (you can't figure out from the log which exact part of long selector had a problem)
- is not lazy, leading to different hacks and workarounds for DRYing the code.
Quick example
So, you get this:
const newTodo = s('#new-todo')
const todos = s('#todo-list>li')
const completed = todos.by('.completed')
const active = todos.not('.completed')
const complete = (todo) => {
todos.by(`:contains(${todo})`).find('.toggle').click()
}
// ...
it('completes todo', () => {
browser.visit('https://todomvc.com/examples/emberjs/')
newTodo.type('a').pressEnter()
newTodo.type('b').pressEnter()
newTodo.type('c').pressEnter()
todos.should(have.exactTexts, 'a', 'b', 'c')
complete('b')
completed.should(have.exactTexts, 'b')
active.should(have.exactTexts, 'a', 'c')
})
Instead of this:
const todosSelector = '#todo-list>li'
const newTodo = () => cy.get('#new-todo')
const todos = () => cy.get(todosSelector)
const completed = () => todos().filter('.completed')
const active = () => todos().not('.completed')
const complete = (todo) => {
todos(`${todosSelector}:contains(${todo}) .toggle`).click()
}
it('completes todo', () => {
cy.visit('https://todomvc.com/examples/emberjs/')
newTodo().type('a{enter}')
newTodo().type('b{enter}')
newTodo().type('c{enter}')
todos().should('have.length', 3)
todos().eq(0).should('have.text', 'a')
todos().eq(1).should('have.text', 'b')
todos().eq(2).should('have.text', 'c')
complete('b')
completed().should('have.length', 1)
completed().eq(0).should('have.text', 'b')
active().should('have.length', 2)
active().eq(0).should('have.text', 'a')
active().eq(1).should('have.text', 'c')
}
Notice that we had to use longer, not broken down selector with additional variable interpolation:
todos(`${todosSelector}:contains(${todo}) .toggle`).click()
This is because in Cypress, the broken down version:
const complete = (todo) => {
todos().filter(`:contains(${todo})`).find('.toggle').click()
}
is not the same as in the «cypress-selene» version:
const complete = (todo) => {
todos.by(`:contains(${todo})`).find('.toggle').click()
}
Because Cypress will not retry the all chain and ensure proper waiting.
Hence, such code is useless:
todos().filter(`:contains(${todo})`).find('.toggle').click()
Moreover, it leads to fragile tests.
We can improve it, though, coming to something like the following:
const complete = (todo) => {
const containsTodo = `:contains(${todo})`
const toggle = '.toggle'
const haveFiltered = (selector) => ($elements) => {
expect($elements.filter(selector).length).to.be.at.least(1)
}
todos()
// needed for better debug:
// to separate error when we have no b from have no .toggle inside
.should(haveFiltered(containsTodo))
// needed for complete stability
// (see https://docs.cypress.io/guides/core-concepts/retry-ability#Alternate-commands-and-assertions)
.should(haveFiltered(`${containsTodo}:has(${toggle})`))
.filter(containsTodo)
.find(toggle)
.click()
}
yet, a lot with additional assertions and variables! And all this – is already built into «cypress-selene» version:
const complete = (todo) => {
todos.by(`:contains(${todo})`).find('.toggle').click()
}
See more examples at integration/examples/todomvc.spec.js and integration/examples/demoqa/studentRegistrationForm.spec.js
Intallation
npm install -D cypress-selene
Then include in your project's cypress/support/index.js
require('cypress-selene')
or
import 'cypress-selene'
For autocomplete and hints support add "cypress-selene"
to the "types"
section of your jsconfig.json
or tsconfig.json
:
{
"compilerOptions": {
"types": [
"cypress",
"cypress-selene",
],
},
}
Disclaimer
The smarter retriability magic added to Cypress as a part of this package will make tests more stable but also slower. Though, some configuration will be added later – to tune "magic to your needs", and turn it off when you don't need it ;) (stay tuned and watch #5)
Main features breakdown
Selene/Selenide's style elements
s(selector)
returning the object of Locator classLocator class as Lazy and Fluent API wrapper
- over
cy.get(selector)
,.filter(selector)
- with
.by(selector)
alias, with additional selector conversions see custom commands explanations below;)
- with
.not(selector)
with additional selector conversions see custom commands explanations below;).find(selector)
.eq(index)
.first()
.last()
.next(selector)
.should(matcher, *args)
- with all methods above
- being lazy, returning same Locator instance with "updated" selector path
- so you can store it in the var, like
const active = new Locator({path: '#todo-list>li'}).not('.completed')
- so you can store it in the var, like
- being fully retriable (cypress only retries the last command)
- with integrated smart waits/assertions per retry so you see in log what was the reason of retry and its failure in the worst case i.e. you can break down long selectors into parts in order to see in the log the exact problematic part and so fasten your tests support ;)
- being lazy, returning same Locator instance with "updated" selector path
- with non lazy commands, that actually find subject to perform actual actions
locator.get()
returning actual cy subject- here the lazyness ends, and all subsequent API is a raw Cypress one, i.e. not lazy, and you can not store it into vars)
- it's usefull to force it to get raw subject and use classic cy command that is not yet available in Locator.*
locator.type(text)
locator.clear()
locator.submit()
locator.setValue(text)
- as alias to
.clear().type(text)
- as alias to
locator.click()
locator.doubleClick()
- as more user-oriented alias to
.dbclick
- as more user-oriented alias to
locator.hover()
- as alias to .trigger('mousover')
locator.pressEnter()
locator.pressEscape()
- over
globals
- ... planned to be removed from globals in newer versions ;)
- Selene's style
browser
as alias tocy
- for more user-oriented
browser.visit('https://url.org')
overcy.visit('https://url.org')
- for more user-oriented
s(selector)
as alias to newLocator({path: selector})
- to allow lazy (also with full retriability)
over not-fully-retriable and lazy-only-via-functionsconst todos = s('#todo-list>li) //... todos.should(/*...*/) todos.filter('.completed').should(/*...*/)
const todos = () => cy.get('#todo-list>li) //... todos().should(/*...*/) todos().filter('.completed').should(/*...*/) // Cypress retries only .filter(...) here
- to allow lazy (also with full retriability)
be.*
andhave.*
- as aliases to some most used cypress «chainers/matchers/conditions»
- for cleaner code (when reviewing it will be easier to distinguish code from test data;):
overs('#todo-list>li').eq(1).should(have.text, 'i am test data, emphasized by quotes;)')
s('#todo-list>li').eq(2).should('have.text', 'of same style as prev arg')
customized commands
- new
cy.the(wordOrSmarterSelector)
in addition tocy.get(selector)
- to consider all words as values of
data-qa
attributes i.e.cy.the('submit')
is same ascy.get('[data-qa=submit]')
support of customizing such «data qa attributes» will be added later #2 - to support Playwright style «search by text»
i.e.
cy.the('text=Press me')
is same ascy.contains('Press me')
- same as
cy.get(selector)
otherwise - support of customizing such conversions will be added later #3
- to consider all words as values of
cy.by(smarterSelector)
as alias tocy.filter(selector)
- for conciseness
- and smarter conversions:
P.S.cy.get('.todo').by(':contains("Write a test!")') cy.get('.todo').by('text=Write a test') // same as above cy.get('.todo').by('.completed') // same as cy.get('.todo').filter('.completed') cy.get('.todo').by(':not(.completed)') // same as cy.get('.todo').not('.completed') cy.get('.todo').by(':has(img.high-priority-flag)') // same as cy.get('.todo').filter(':has(img.high-priority-flag)') cy.get('.todo').by(' img.high-priority-flag') // same as above cy.get('.todo').by(':has(>img.high-priority-flag)') cy.get('.todo').by('>img.high-priority-flag') // same as above
s(selector)
is also available for even conciser:s('.todo').by('text=Write a test') s('.todo').by('.completed')
- overwritten
cy.not
for same conversions as in customcy.by
- new
customized conditions (matchers)
- new
have.texts(...partialValues)
- for conciser version
over raw cypresss('#todo-list li').should(have.texts, 'a', 'c')
... 🤦🏻♂️ – check real official examplecy.get('@todo-list li').as('todos') //... cy.get('@todos').should('have.length', 2) cy.get('@todos') .eq(0) .should('contain', 'a') cy.get('@todos') .eq(1) .should('contain', 'c')
- for conciser version
have.exactTexts(...values)
have.elements(selector)
orhave.the(selector)
- as alias to
.should(($elements) => { expect($elements.has(selector).length).to.be.gt(0) })
- as alias to
have.filtered(selector)
- as alias to
.should(($elements) => { expect($elements.filter(selector).length).to.be.gt(0) })
- as alias to
- changed in alias (have.* or be.*)
- for better readability according to native english in
be.*
stylebe.equalTo
over'equal'
be.matching
over'match'
be.containing
over'contain'
have.valueContaining
over'contain.value'
be.inDOM
over'exist'
for less confusion in understanding from the user perspectivehave.text
over'include.text'
as in Selenide/Selene, because in native english «have text» naturally means «have some text inside»have.exactText
over'have.text'
as in Selenide/Selene,have.cssClass
over'have.class'
as in Selenide/Selene for less confusion, because «class» is also the whole attribute that can contain many «css classes»
- for better readability according to native english in
- new
Differences from other testing libraries
Differences from raw Cypress
- you can store locators to vars like `const ok = s('#ok')
- all retry-able queries (like filter, find, etc.) written in a chain before first action – are retried (in raw Cypress only the last query is retried)
- yet first action (e.g. type, click) on query (built as a chain of get filter find, etc) potentially break retry-ability for next actions
- so remember and count that full retry-ability will work only for the first action
- TODO: consider removing this limitation by implementing: store a chain of commands and call it only ON next cy.get or cy.request (etc.) OR custom cy.end()... or find another way:)
- yet first action (e.g. type, click) on query (built as a chain of get filter find, etc) potentially break retry-ability for next actions
- all retry-able queries (like filter, find, etc.) written in a chain before first action – are retried (in raw Cypress only the last query is retried)
- you can write
.should(have.length, 3)
instead.should('have.length', 3)
Differences from Selenide & Co
cy.get(selector)
or this project extensions(selector)
– is not an element but a Locator that can resolve under the hood into element or collection depending on context. There is no explicit way to differentiate a "collection of elements" and "element".- $ in Selenide from Java is not the same as $ here, where it's a method from JQuery lib, that allows similar things but on a lower level of DOM manipulation. Instead of Selenide's
$(selector)
– uses(selector)
here, same way as in Selene from Python ;) – yet, count that it's not a lazy element, it's lazy Locator that can find more than one element;)
Differences from Playwright and other async libs for web ui test automation
cy.*.*.*
is kind of actions builder, that, being async under the hood – looks like syncronous and should be used like syncronous- e.g. playwright or webdriver.io - are async and should be used as async ;)