Access element whose parent is hidden - cypress.io

After much nashing-of-teeth, I think I have an answer.

I think the root cause is that mdc-select-item has display:flex, which allows it to exceed the bounds of it's parents (strictly speaking, this feels like the wrong application of display flex, if I remember the tutorial correctly, however...).

Cypress does a lot of parent checking when determining visibilty, see visibility.coffee,

## WARNING:
## developer beware. visibility is a sink hole
## that leads to sheer madness. you should
## avoid this file before its too late.
...
when $parent = parentHasDisplayNone($el.parent())
  parentNode = $elements.stringify($parent, "short")

  "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
...
when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
  parentNode  = $elements.stringify($parent, "short")
  width       = elOffsetWidth($parent)
  height      = elOffsetHeight($parent)

  "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."

But, when using .should('be.visible'), we are stuck with parent properties failing child visibility check, even though we can actually see the child.
We need an alternate test.

The work-around

Ref jquery.js, this is one definition for visibility of the element itself (ignoring parent properties).

jQuery.expr.pseudos.visible = function( elem ) {
  return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}

so we might use that as the basis for an alternative.

describe('Testing select options', function() {

  // Change this function if other criteria are required.
  const isVisible = (elem) => !!( 
    elem.offsetWidth || 
    elem.offsetHeight || 
    elem.getClientRects().length 
  )

  it('checks select option is visible', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()

    //cy.get('mdc-select-item').contains("ITEM1").should('be.visible') //this will fail
    cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
      expect(isVisible(item1[0])).to.be.true
    });
  });

  it('checks select option is not visible', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()

    cy.document().then(function(document) {

      const item1 = document.querySelectorAll('mdc-select-item')[0]
      item1.style.display = 'none'

      cy.get('mdc-select-item').contains("ITEM1").then (item => {
        expect(isVisible(item[0])).to.be.false
      })
    })
  });

  it('checks select option is clickable', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()
    
    //cy.get('mdc-select-item').contains("ITEM1").click()    // this will fail
    cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
    
      cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
        expect(isVisible(item2[0])).to.be.true  //visible when list is first dropped
      });
          
      item1.click();
      cy.wait(500)
          
      cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
        expect(isVisible(item2[0])).to.be.false  // not visible after item1 selected
      });
    });
    
  })

Footnote - Use of 'then' (or 'each')

The way you normally use assertion in cypress is via command chains, which basically wraps the elements being tested and handles things like retry and waiting for DOM changes.

However, in this case we have a contradiction between the standard visibility assertion .should('be.visible') and the framework used to build the page, so we use then(fn) (ref) to get access to the unwrapped DOM. We can then apply our own version of the visibility test using stand jasmine expect syntax.

It turns out you can also use a function with .should(fn), this works as well

it('checks select option is visible - 2', function() {
  const doc = cy.visit('http://localhost:4200')
  cy.get("mdc-select").contains("installation type").click()

  cy.get('mdc-select-item').contains("ITEM1").should(item1 => {
    expect(isVisible(item1[0])).to.be.true
  });
});

Using should instead of then makes no difference in the visibility test, but note the should version can retry the function multiple times, so it can't be used with click test (for example).

From the docs,

What’s the difference between .then() and .should()/.and()?

Using .then() simply allows you to use the yielded subject in a callback function and should be used when you need to manipulate some values or do some actions.

When using a callback function with .should() or .and(), on the other hand, there is special logic to rerun the callback function until no assertions throw within it. You should be careful of side affects in a .should() or .and() callback function that you would not want performed multiple times.

You can also solve the problem by extending chai assertions, but the documentation for this isn't extensive, so potentially it's more work.


I came across this topic but was not able to run your example. So I tried a bit and my final solution is this. maybe someone other also needs this. Please note that I use typescript.

First: Define a custom command

Cypress.Commands.add("isVisible", { prevSubject: true}, (p1: string) => {
      cy.get(p1).should((jq: JQuery<HTMLElement>) => {
        if (!jq || jq.length === 0) {
            //assert.fail(); seems that we must not assetr.fail() otherwise cypress will exit immediately
            return;
        }

        const elem: HTMLElement = jq[0];
        const doc: HTMLElement = document.documentElement;
        const pageLeft: number = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
        const pageTop: number = (window.pageYOffset || doc.scrollTop)  - (doc.clientTop || 0);
        let elementLeft: number;
        let elementTop: number;
        let elementHeight: number;
        let elementWidth: number;

        const length: number = elem.getClientRects().length;

        if (length > 0) {
            // TODO: select correct border box!!
            elementLeft = elem.getClientRects()[length - 1].left;
            elementTop = elem.getClientRects()[length - 1].top;
            elementWidth = elem.getClientRects()[length - 1].width;
            elementHeight = elem.getClientRects()[length - 1].height;
        }

        const val: boolean = !!( 
            elementHeight > 0 && 
            elementWidth > 0 && 
            elem.getClientRects().length > 0 &&
            elementLeft >= pageLeft &&
            elementLeft <= window.outerWidth &&
            elementTop >= pageTop &&
            elementTop <= window.outerHeight
        );

        assert.isTrue(val);
      });
});

Please note the TODO. In my case I was targeting a button which has two border boxes. The first with height and width 0. So i must select the second one. Please adjust this to your needs.

Second: Use it

cy.wrap("#some_id_or_other_locator").isVisible();

For convenience and reusability I had to mix the answer of Richard Matsen and Josef Biehler.

Define the command

// Access element whose parent is hidden
Cypress.Commands.add('isVisible', {
  prevSubject: true
}, (subject) => {
  const isVisible = (elem) => !!(
    elem.offsetWidth ||
    elem.offsetHeight ||
    elem.getClientRects().length
  )
  expect(isVisible(subject[0])).to.be.true
})

You can now chain it from contains

describe('Testing select options', function() {
  it('checks select option is visible', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()

    //cy.get('mdc-select-item').contains("ITEM1").should('be.visible') // this will fail
    cy.get('mdc-select-item').contains("ITEM1").isVisible()
  });
});