Using latest Cypress (14.3.0), I have a test with many lines like this (simplified example):
cy.contains("a", "foo").as("a").click().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:
Cypress.Commands.add("clickAndFind", { prevSubject: "element" }, (subject, selector) => {
cy.wrap(subject).as("a").click()
return cy.get("@a").find(selector)
})
But this does not work, because aliasing the wrapped HTMLElement
is not the same as calling as
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? I mean other than simply pulling out the as
command, which makes the call look ugly/complicated like this:
cy.contains("a", "foo").as("a").clickAndFind("a", "span.bar")
After looking into the source code of the “as” command, I came up with this solution:
Cypress.Commands.add("clickAndFind", { prevSubject: "element" }, (subject, selector) => {
const alias = "clickAndFind"
const command = cy.state("current").get("prev")
const fileName = command.get("fileName")
const subjectChain = [...cy.subjectChain()]
cy.addAlias(cy.state("ctx"), { alias, command, fileName, subjectChain })
cy.wrap(subject).click()
return cy.get("@" + alias).find(selector)
})
Usage example:
cy.contains("a", "foo").clickAndFind("span.bar")
Note: Some commands used in the code are not exposed in types. So if using TypeScript, you will get IDE errors like:
Property 'state' does not exist on type 'cy & CyEventEmitter'.ts(2339)
As a quick workaround to this I defined those commands (with bogus types):
declare global {
namespace Cypress {
interface Chainable {
/**
* @deprecated don't call outside this file. It is only defined so we don't get errors in our clickAndFind implementation
*/
state: (s: string) => any
/**
* @deprecated don't call outside this file. It is only defined so we don't get errors in our clickAndFind implementation
*/
subjectChain: () => any
/**
* @deprecated don't call outside this file. It is only defined so we don't get errors in our clickAndFind implementation
*/
addAlias: (ctx: any, options: any) => void
/**
* 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
}
}
}