- Ask yourself: Will I understand this after a month?
cy.setCookie("cookie_consent", "agreed")
cy.visit("…")
cy.visit("»some slow page«")
// Sub-optimal
cy.wait(10 * 1000)
cy.get("…").click()
// Good
cy.get("…", { timeout: 14 * 1000 }).click()
// Also good
// Change defaultCommandTimeout in configuration to 14000
cy.get("…").click()
// Bad
cy.visit("some slow page")
cy.get("element load").wait(10000).click()
// This is actually not doing what it looks it's doing
// It's trying to find element with default timeout (4s), THEN waiting 10s, and THEN clicking
// So if the element won't be on the page in 4s, test will fail
// Better, if needed. But still prefer timeout
cy.visit("some slow page")
cy.wait(10000)
cy.get("element load").click()
- TODO: Styled components,
Foo__StyledFoo_xyz
- TODO: Selecting dynamic classes (invalid, touched, ...)
What are these weird "sc-bdVaJa cXeDc" classes?
Nowadays, it's common to generate CSS instead of manually writing it.
One of the most popular examples of this technique is Styled components (Used at Kiwi.com)
const Box = styled.div`
background: salmon;
height: 100px
`
<Box />
<div class="sc-bdVaJa cXeDcp"></div>
.cXeDcp {
height: 100px;
background: salmon;
}
This approach has many advantages (mostly out of scope for this explanation), but comes at a cost of mangled class names obstructing selecting them in automation tools, like Cypress.
Fortunately, Styled components allows setting of "nicer classes for debugging" and Kiwi.com's Orbit components are using it.
<button class="Button__StyledButton-sc-1brqp3f-1 jPZlME">
This generated class as a whole will still change often (even every release), and therefore cannot be used as selector.
But the beginning of the class Button_
will stay the same and can be used.
Playground for Styled components: https://jsbin.com/mizijaz/edit?html,output
// Bad
cy.get("[class='BookingSeat__Container-sc-1qu37au-0 ewtsmp']").click()
// Good
cy.get("[class^='BookingSeat_']").click() // ^= means "starts with"
_
is very important
<div class="BookingSeating__Container-xyz123">
<div class="BookingSeat__Container-xyz123"></div>
</div>
cy.get("[class^='BookingSeat']") // 🔴 This will select BookingSeating
cy.get("[class^='BookingSeat_']") // ✅ This will select BookingSeat
^=
with shorter dot syntax .
for selecting classes.
// This will not work!!!
cy.get(".^PaymentButton_']").click()
cy.get("[data-tkey='booking.global.agreement.text_new2']").check() // 😐
cy.get(".ReservationAgreement checkbox").check() // 🙏
// Suboptimal
cy.get(".InsuranceOption:last").click()
// Good
cy.get(".InsuranceOption:last").click() // Select "Premium insurance"
cy.get(".BookingPassengerEditSummaryInfo .BookingPassengerEditSummaryInfo-wrap-single._original")
// ==>
cy.get(".BookingPassengerEditSummaryInfo-wrap-single._original")
.Hotels .Ad .Title .Actions {}
.Hotels-Ad-Title-Actions {}
cy
.get(".SpecialAssistanceFormComponent")
.find("button")
.click()
// ==>
cy
.get(".SpecialAssistanceFormComponent button")
.click()
cy.get("[src='/images/flags/spFlag-cz.png']")
// ===>
cy.get("[src$='cz.png']")
<div class="PaymentFormCard">
<input
type="text"
name="payment.card.number"
autocomplete="cc-number"
data-test="payment-cc-number-input"
/>
<!-- … more inputs -->
</div>
cy.get(".PaymentFormCard [autocomplete='cc-number']").type("...")
cy.get("input[name='payment.card.number']").type("...")
cy.get("[data-test='payment-cc-number-input']").type("...")
All are valid and acceptable selectors, but for clarify, choose one and stick to it.
Another example
// Suboptimal
cy.get("[type=email]").type("test@example.com")
cy.get("[name='contact.phone']").type("123456789")
// Good
cy.get("[name='contact.email']").type("test@example.com")
cy.get("[name='contact.phone']").type("123456789")
<select name="gender">
<option value="mr">Hombre</option>
<option value="ms">Mujer</option>
</select>
<select name="birthMonth">
<option value="01">Leden</option>
<option value="02">Únor</option>
<!-- ... -->
</select>
// Sub-optimal
cy.get("select[name='gender']").select("Hombre")
cy.get("select[name='birthMonth']").select("Leden")
// Good
cy.get("select[name='gender']").select("mr")
cy.get("select[name='birthMonth']").select("01")
Few it
s
it("title", () => {
cy.veryClearCommand()
veryClearFunction()
cy.log("comment what next command does")
notSoDescriptiveFunction()
})
Lot of it
s
describe("title", () => {
it("very clear description", () => {
cy.veryClearCommand()
})
it("another very clear description", () => {
cy.anotherVeryClearCommand()
})
it("description of not descriptive command", () => {
cy.notSoDescriptive()
})
})
// BAD
describe("Payment", () => {
it("loads", () => {
cy.visit()
})
it("checks something", () => { // .only not possible to add
cy.get().click()
})
})
// GOOD
describe("Payment", () => {
before(() => {
cy.visit()
})
it("checks something", () => { // .only possible to add
cy.get().click()
})
})
Commented code is not treated as code in editors and it will not be considered in linting / refactorings / searching for usages.
function goToPayment() {
cy.get(".Booking-dialog .Button").click()
}
it.skip("Something", () => {
goToPayment() // Editor handles this
})
/*
it("Something", () => {
goToPayment() // Editor does not handle this
})
*/
In order to keep tests clean and succinct, focus on the feature under the test and prepare your environment is such a way it doesn't interfere much with outside world.
describe("Auth", () => {
before(() => {
cy.setCookie("cookie_consent", "agreed")
cy.setCookie("newsletter_prompt", "hide")
cy.setCookie("native_app_banner", "hide")
cy.visit("…")
})
it("Signup", () => { /* ... */ })
it("Login with credentials", () => { /* ... */ })
it("Login with Facebook", () => { /* ... */ })
it("Login with Google", () => { /* ... */ })
it("Forget password", () => { /* ... */ })
// ...
})
If integration with other features is important, cover it separately.
describe("Auth: integration with other components", () => {
it("Integration with Cookie consent", () => { /* ... */ })
it("Integration with Sidebar", () => { /* ... */ })
it("Integration with Native app banner", () => { /* ... */ })
// ...
})
// Suboptimal
cy.get("button").contains("Continue").click()
cy.get("button").contains("Continue").click()
cy.get("button").contains("Continue").click()
// Good
cy.get("button").contains("Continue").click() // Continue to "Shipping"
cy.get("button").contains("Continue").click() // Continue to "Overview"
cy.get("button").contains("Continue").click() // Continue to "Payment"
// Perfect - will be shown also in Cypress UI
cy.log("Continue to 'Shipping'")
cy.get("button").contains("Continue").click()
cy.log("Continue to 'Overview'")
cy.get("button").contains("Continue").click()
cy.log("Continue to 'Payment'")
cy.get("button").contains("Continue").click()
// Suboptimal
cy.get("button").click().click()
// Good
cy.get("button").click().click() // Add two items
// TODO: Explain
Cypress.on(
"uncaught:exception",
err => err.message.indexOf("Cannot set property 'aborted' of undefined") === -1,
)
cy.get("[name='cardExpirationYear']").type("20", { force: true }) // it's weirdly covered by ...
describe.skip("…", () => { // Disabled due to flakiness // TODO: Solve it and un-skip
it.skip("…", () => { // Feature is temporarily disabled, un-skip when enabled
Prefer simple Javascript functions over Cypress commands.
Use cypress commands only if:
- chaining, and using subject returned from previous command
- chaining, and providing returned subject for next commands in chain
- 100% sure you want to extend
cy
otherwise, use simple javascript functions
Bad
// mmbCommands.js
Cypress.Commands.add("mmbLoad", mock => {
cy
.visit(`/en/manage/123456789/${mock}?override_api=true}`)
.get(".BookingHeader")
.wait(500) // wait for ancilliaries api calls
})
// some-tests-spec.js
describe("MMB: Check-in", () => {
before(() => {
cy.mmbLoad("default:default")
})
Good
// mmbCommands.js
export function load(mock) {
cy
.visit(`/en/manage/123456789/${mock}?override_api=true}`)
.get(".BookingHeader")
.wait(500) // wait for ancilliaries api calls
}
// some-tests-spec.js
import * as mmbCommands from "./../mmbCommands"
describe("MMB: Check-in", () => {
before(() => {
mmbCommands.load("default:default")
})
Why? Awesome support in IDE and static analysis!
- Find usages
- Go to definition
- Quick definition
- Highlight errors
- ...many others
// Bad
function mmbIsAccountBookingListVisible() {
cy.get(".AccountBookingList")
})
// Bad
Cypress.Commands.add("haveLength", (elements, length) => {
expect(elements.length).to.equal(length)
})
cy.get("something").haveLength(3)
// Good
cy.get("something").should("have.length", 3)
// Sub-optimal
cy.get("input[type='checkbox'][required]").each($el => { // T&C, GDPR and Adult checkboxes
$el.click()
})
// Better
cy.get("input[type='checkbox'][required]").click({ multiple: true }) // T&C, GDPR and Adult checkboxes
// Sub-optimal
cy.get("input").each($el => {
cy.wrap($el).should("have.class", "_disabled")
})
// Better, simpler
cy.get("input").should("have.class", "_disabled")
cy.get(".navbar .logo").should("exist")
cy.get(".navbar").find(".logo").should("exist")
// is same as
cy.get(".navbar .logo")
cy.get(".navbar").find(".logo")
(Slack announcement)
Reasoning
Without { }
, expression is implicitly returned.
When returned value is promise-like (and almost all cypress commands are),
Mocha switches to async mode and requires explicit completion
(usually via done()
callback)
Cypress is doing some behind-the-scenes magic to work even without { }
,
but it's not standard and
it's breaking other standardised, well behaved, tools (like test coverage)
Example
// Bad, bad boy
it("some check", () =>
cy.verySophisticatedCommand("Foo", "Whoo"))
// Good citizen
it("some check", () => {
cy.verySophisticatedCommand("Foo", "Whoo"))
}