To start, I am working with react-swipe-to-delete-component, but I feel that should make little difference in the question.
I am trying to change the background-color property of a "built out" element. I have no control over the class name as the plugin uses it, and I cannot assign it (I think) a separate class name as it stands. Is there a way to change the style of a element solely based on selector, like in vanilla JavaScript?
I have this element <SwipeToDelete>. It builds out a div that I need to access by the selector:
<div class="swipe-to-delete">
<div class="js-delete">
And thus I need to be able to access
.swipe-to-delete .js-delete
as a selector.
Is there a way like in JS IE document.getElementsByClassName('.swipe-to-delete .js-delete')[0].style.background-color = "#FFF")
I need to change the background color based on which function is called:
const [isRight, setRight] = useState(false);
const [isLeft, setLeft] = useState(false);
const onLeft = (...args) => {
// Make Mask Red with Icon
console.log('Set left to true');
setLeft(true);
}
const onRight = (...args) => {
// Make Mask Green with icon
console.log('Set right to true');
setRight(true);
}
<SwipeToDelete
onLeft={onLeft}
onRight={onRight}
>
This works.. I just don't know how to incorporate this pseudo statement
isRight ? '.swipe-to-delete .js-delete'.backgroundColor="green" : ''
Since I can't access <SwipeToDelete>'s inner workings to set a class name somewhere, without modifying the node-module code. I really am stuck here.
I just want to simply change the background color of .swipe-to-delete .js-delete depending on right or left function. Am I looking at this the wrong way?
There is a classNameTag prop you can use to define a className on the wrapper element for <SwipeToDelete> (the element with className="swipe-to-delete").
Based on the swipe direction you set the className accordingly:
<SwipeToDelete
classNameTag={isRight ? "is-right" : isLeft ? "is-left" : ""}
onLeft={onLeft}
onRight={onRight}
>
And styling with CSS:
.swipe-to-delete.is-right .js-delete {
background-color: green;
}
.swipe-to-delete.is-left .js-delete {
background-color: red;
}
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?
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.
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()
});
});
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();
I could solve it by calling scrollIntoView after getting an element. See this answer.
A related problem:
Cypress was unable to find a tab element because it had a style of display: none (even though it was visible on the page)
My workaround:
Cypress could target the tab by matching text and clicking
cy.get("[data-cy=parent-element]").contains("target text").click();
To expand a bit the answer of BTL, if anyone faced an error - Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>> in Typescript, following is what I added at the top of commands.ts in cypress to get away with it -
declare global {
namespace Cypress {
interface Chainable {
isVisible;
}
}
}
And may be replacing expect(isVisible(subject[0])).to.be.true with assert.True(isVisible(subject[0])); if you see any chai assertion error with expect and don't want to import it - as in Josef Biehler answer..
I was facing the same error that parent is hidden so Cypress is unable to click the child element, I handled this by handling the visibility of parent from hidden to visible by this code
cy.get('div.MuiDrawer-root.MuiDrawer-docked').invoke('css', 'overflow-x', 'visible').should('have.css', 'overflow-x', 'visible')
Note: You can apply any css you want in the invoke function like I have
Remove the flex and try. If it is solved then use the flex standard way
How can I reset an elements 'class' attribute to it's initial value?
I am building a tooltip popup which starts with class="ttPopup". This is then set to the appropriate orientation by adding classes such as class="ttPopup top left".
Problem is when the Popup windows closes, how do I reset the class to it's original value ready for the next time?
There are several ways you could do it:
store in a custom attribute
store in a javascript array
store in localStorage
etc.
Not completely sure if I am correct to use a custom property on the element or not but here is the solution I have used at the moment:
eTooltip.addEventListener("mouseenter", function (oEvent) { ttOpen(oEvent); } );
eTooltip.addEventListener("mouseleave", function (oEvent) { ttClose(oEvent); } );
function ttOpen(oEvent) {
var thisPopup = oEvent.target.getElementsByClassName("ttPopup")[0];
thisPopup.origClassName = thisPopup.className;
}
function ttClose(oEvent) {
var thisPopup = oEvent.target.getElementsByClassName("ttPopup")[0];
if (thisPopup.origClassName) { thisPopup.className = thisPopup.origClassName; thisPopup.origClassName = null; }
console.log(thisPopup.className)
}
Thanks for your help.
Lets say I have a react DOM element like this one:
render () {
...
return (
<div className = "widePaddingRight" style = {{width: "100px"}}
ref = "pickMe"
>
...
</div>
)
}
CSS:
.widePaddingRight{
paddingRight: 20px
}
If I access this element later and try to get its width like that:
componentDidMount () {
var elem = this.refs.pickMe.getDOMNode().getBoundingClientRect()
console.log(elem.width)
}
I get 120 in my console. My expected result was 100. Since I have to calculate with the original width I have to get the padding-attributes of the element.
Question: How can I get the paddingRight attribute of my component in my react-class?
Update
With the input of #Mike 'Pomax' Kamermans I was able to solve the underlying problem (thank you for that): Just add box-sizing: border-box to the CSS. Now getBoundingClientRect() gives 100 instead of 120.
I still dont know how to get a css class-attribute from a mounted div - any suggestions?
You need window.getComputedStyle.
const style = window.getComputedStyle(React.findDOMNode(this.refs.pickMe));
How reset label property.
It means, initially we set the cursor property as #Hand Pointer, then i want to change cursor property value as reset. i should not get hand symbol while doing some other operation. it should be fully disabled.
i tried one way but its not working.
function disableCancelLbl() {
log.info("inside disableCancelLbl");
var lblCancel = createCustomer.getServer().getJSXByName("lbl_ctId_cancel");
lblCancel.setEvent("", jsx3.gui.Interactive.JSXCLICK);
lblCancel.setClassName("buttonTextStyleOff");
lblCancel.setCursor("default",true);
log.info(lblCancel.getCursor());
lblCancel.repaint();
//reset(lblCancel.getCursor());
log.info(lblCancel.getCursor());
}
after repainting, again its changing to hand pointer.
Here's what I use to set/reset the cursor on a button. The setCursor methods seem to work, but the reset to default doesn't work for Chrome :-(
rsh.setWaitCursor = function(button)
{
button.setEnabled(jsx3.gui.Form.STATEDISABLED); button.repaint();
// WaitCursor reset not working in Chrome !!
// 'root' not overwriting wait cursor !
if( !rsh.isChrome ) {
button.setCursor("wait",true);
}
}
rsh.resetWaitCursor = function(button)
{
button.setEnabled(jsx3.gui.Form.STATEENABLED); button.repaint();
// WaitCursor reset not working in Chrome !!
if( !rsh.isChrome ) {
button.setCursor("default",true);
}
}