Access element whose parent is hidden - cypress.io

The question, as indicated in the title, i.e. To access an element whose parent is hidden. The problem is that according to the cypress.io docs :

An element is considered hidden if:

  • Its width or height is 0.
  • CSS Property (or its ancestors) - Visibility: Hidden.
  • CSS Property (or its ancestors): display: none.
  • Its CSS property is position: fixed, and its off-screen or hidden.

But the code I'm working with requires me to click on an element whose parent is hidden and the element itself is visible .

Therefore, every time I try to click on an item, an error message appears:

CypressError: Retry timeout: expected "<mdc-select-item # mdc-select-item-4.mdc-list-item>" to be "visible"

This element <mdc-select-item # mdc-select-item-4.mdc-list-item> is invisible because its parent element <mdc-select-menu.mdc-simple-menu.mdc-select__menu> 'has the CSS property : 'display: none'

enter image description here

The element I'm working with is the dropdown item , which is written in pug . An element is a component defined in angular-mdc-web that uses mdc-select for the drop down menu and mdc-select-item for its items, which I must access.

Example code of a similar structure:

 //pug mdc-select(placeholder="installation type" '[closeOnScroll]'="true") mdc-select-item(value="false") ITEM1 mdc-select-item(value="true") ITEM2 

Above ITEM1 is an element, I have access to ITEM1 . I do this in cypress.io as follows:

 //cypress.io // click on the dropdown menu to show the dropdown (items) cy.get("mdc-select").contains("installation type").click(); // try to access ITEM1 cy.get('mdc-select-item').contains("ITEM1").should('be.visible').click(); 

We tried using {force:true} click an item, but no luck. I tried to select elements by pressing the {enter} key on the parent mdc-select , but again, no luck, as it produces:

CypressError: cy.type () can only be called for textarea or: text. Your topic: <mdc-select-label class = "mdc-select__selected-text"> Select ... </mdc-select-label>

I also tried to use the select command, but this is not possible because the Cypress engine cannot identify an element as a select element (because it is not, the inner workings are different). It throws:

CypressError: cy.select () can only be called for a. Your topic: <mdc-select-label class = "mdc-select__selected-text"> Select ... </mdc-select-label>

The problem is that mdc-select-menu , which is the parent of mdc-select-item has the display:none property from internal calculations when opening drop-down items.

enter image description here

This property is overwritten for display:flex , but that doesn't help.

enter image description here

All of the ideas. This works in Selenium , but not with cypress.io . Any hint that it might be possible to hack into a situation other than switching to other platforms or changing the user interface code?

+11
javascript angular ui-automation pug cypress
source share
4 answers

I think that after much beating, I have an answer.

I think the main reason is that mdc-select-item has display:flex , which allows it to go beyond the boundaries of its parents (strictly speaking, this is like misusing display flex if I remember the tutorial correctly, however .. .).

Cypress does a lot of parenting when determining visibility, 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 the parent properties, not passing the child visibility check, even if we can actually see the child.
We need an alternative test.

Workaround

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

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

therefore, we can use this as a 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 - Using "then" (or "each")

You typically use the statement in Cypress through chains of commands that basically wrap the elements being checked and handle things like repeating and waiting for DOM changes.

However, in this case, we have a contradiction between the standard statement of visibility .should('be.visible') and the structure used to create the page, so we use then(fn) ( ref ) to access the expanded DOM. Then we can apply our own version of the visibility test using the Jasmine syntax.

Turns out you can also use the function with .should(fn) , this works too

 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 does not make a difference in the visibility test, but note that the should version can repeat the function several times, so it cannot be used with the click test (for example).

From the documents

What is the difference between .then () and .should () /. AND()?

Using .then () just allows you to use the resulting item in a callback function and should be used when you need to manipulate some values ​​or perform some actions.

When using the callback function with .should () or .and (), on the other hand, there is special logic to re-run the callback function until statements are added to it. You should be careful with side effects in the .should () or .and () callback function, which should not be executed multiple times.

You can also solve this problem by expanding the basic statements, but the documentation on this is not extensive, so it can potentially work more.

+8
source share

In docs Cypress selection syntax, syntax

 cy.get('mdc-select-item').select('ITEM1') 

You may need {force: true} . See here select_spec.coffee for examples of your own tests, e.g.

 it "can forcibly click even when element is invisible", (done) -> select = cy.$$("select:first").hide() select.click -> done() cy.get("select:first").select("de_dust2", {force: true}) 
+1
source share

I came across this topic but could not run your example. So I tried a little, and my final decision is as follows. maybe someone needs it too. Please note that I use typewriting.

First: define a user 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 pay attention to TODO. In my case, I aimed at a button that has two frames. The first with a height and width of 0. Therefore, I must choose the second. Please customize this according to your needs.

Second: use this

 cy.wrap("#some_id_or_other_locator").isVisible(); 
+1
source share

For convenience and reuse, I had to mix the answer of Richard Matsen and Joseph Beehler.

Define a team

 // 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 }) 

Now you can tie it from

 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() }); }); 
0
source share

All Articles