cypress

Aliases in custom commands


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")

Solution

  • 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
          }
       }
    }