Using latest Cypress (14.3.0), I have a test with many lines like this (simplified example):
cy.contains("a", "foo").as("a").click()
cy.get("@a").find("span.bar")
It clicks on the (first) a element containing the text "foo" and then expects the specified span element to appear inside that a element. The alias is needed, because the DOM changes after the click.
To clean things up I am trying to have a custom command to be used like this:
cy.contains("a", "foo").clickAndFind("span.bar")
My command code (first attempt):
Cypress.Commands.add("clickAndFind", { prevSubject: "element" }, (e, s) => {
cy.wrap(e).as("alias").click()
return cy.get("@alias").find(s)
})
This does not work for me, because the given HTMLElement e does not contain any DOM queries and thus wrapping and aliasing it is not the same as calling as on the command chain (directly after contains). Thus I get the The subject is no longer attached to the DOM error.
Is there any way to get this to work as intended?
Edit: It turns out I simplified a little to much. The following is an only slightly simplified excerpt of the real test code. I post this to show that it's not enough to just rerun the last query without respecting the full command chain.
const sortUp = "span.fa-arrow-up-z-a"
const sortDown = "span.fa-arrow-down-a-z"
cy.contains(".accordion-header", "Patients")
.siblings(".accordion-collapse").as("area")
cy.get("@area").find("thead tr.headers").as("headers")
cy.get("@area").find("tbody").as("patients")
cy.get("@headers").contains("a", "Patient").clickAndFind(sortUp)
cy.get("@headers").contains("a", "Patient").clickAndFind(sortDown)
cy.get("@patients").contains("tr", patient).should("have.class", "table-warning")
user16883681's solution does not always work, because rerunning only the last query gives me the wrong element back if that query also matches an element further up on the page. A proper solution should respect all queries of the command chain.
After looking into the source code of the “as” command, I came up with this solution:
Cypress.Commands.add("clickAndFind", { prevSubject: "element" }, (e, s) => {
const alias = addAlias()
cy.wrap(e).click()
return cy.get(alias).find(s)
})
function addAlias(alias = Date.now().toString()) {
// @ts-ignore
const command = cy.state("current").get("prev")
const fileName = command.get("fileName")
// @ts-ignore
const subjectChain = cy.subjectChain() ? [...cy.subjectChain()] : undefined
// @ts-ignore
cy.addAlias(cy.state("ctx"), { alias, command, fileName, subjectChain })
return "@" + alias
}
Usage example:
cy.contains("a", "foo").clickAndFind("span.bar")
Note: The // @ts-ignore lines are in case you are using TypeScript. Some commands used in the code are not exposed in types. So otherwise you would get IDE errors like:
Property 'state' does not exist on type 'cy & CyEventEmitter'.ts(2339)
Then you might also want to add cy.clickAndFind to its interface:
declare global {
namespace Cypress {
interface Chainable {
/**
* Clicks on the given element and then finds
* child element(s) by the specified CSS selector.
* This even works if the DOM changes after the click.
*
* @param {string} selector - a CSS selector, e.g. "span.fa-arrow-up-z-a"
*/
clickAndFind(selector: string): Chainable
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With