The question is as given in the title, ie, to access element whose parent is hidden. The problem is that, as per the cypress.io docs :
An element is considered hidden if:
- Its width or height is 0.
- Its CSS property (or ancestors) is visibility: hidden.
- Its CSS property (or ancestors) is display: none.
- Its CSS property is position: fixed and it’s offscreen or covered up.
But the code that I am working with requires me to click on an element whose parent is hidden, while the element itself is visible.
So each time I try to click on the element, it throws up an error reading :
CypressError: Timed out retrying: 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 not visible because its parent '< mdc-select-menu.mdc-simple-menu.mdc-select__menu>' has CSS property: 'display: none'
The element I am working with is a dropdown item
, which is written in pug
. The element is a component defined in angular-mdc-web, which uses the mdc-select
for the dropdown menu and mdc-select-item
for its elements (items) which is what I have to access.
A sample code of similar structure :
//pug
mdc-select(placeholder="installation type"
'[closeOnScroll]'="true")
mdc-select-item(value="false") ITEM1
mdc-select-item(value="true") ITEM2
In the above, ITEM1
is the element I have to access. This I do 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();
Have tried with {force:true}
to force the item click, but no luck. Have tried to select the items using {enter}
keypress on the parent mdc-select
, but again no luck as it throws :
CypressError: cy.type() can only be called on textarea or :text. Your subject is a: < mdc-select-label class="mdc-select__selected-text">Select ...< /mdc-select-label>
Also tried using the select
command, but its not possible because the Cypress engine is not able to identify the element as a select
element (because its not, inner workings are different). It throws :
CypressError: cy.select() can only be called on a . Your subject is a: < mdc-select-label class="mdc-select__selected-text">Select ...< /mdc-select-label>
The problem is that the mdc-select-menu
that is the parent for the mdc-select-item
has a property of display:none
by some internal computations upon opening of the drop-down items.
This property is overwritten to display:flex
, but this does not help.
All out of ideas. This works in Selenium
, but does not with cypress.io
. Any clue what might be a possible hack for the situation other than shifting to other frameworks, or changing the UI code?
Cypress has another technique for handling hidden elements. For example, to click a hidden element we can use the Cypress command click and pass the option {force : true} as a parameter to it - click({ force: true }). This modifies the hidden characteristics of the hidden element and we can click it.
The only way for you to "see" and debug why Cypress thought an element was not visible is to use a debugger statement. We recommend placing debugger or using the . debug() command directly BEFORE the action.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With