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()
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 (attempt):

Cypress.Commands.add("clickAndFind", { prevSubject: "element" }, (e, s) => {
   cy.wrap(e).as("alias").click()
   return cy.get("@alias").find(s)
})

But this does not work, because aliasing the wrapped HTMLElement 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")

Solution

  • I like the answer of Leta Waldergrave, but in certain cases it doesn't work. This is 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
          }
       }
    }