WebDriverError: element click intercepted: Other element would receive the click - detection - javascript

I would like to detect if an element is clickable before attempting to click it. In my specific use case the element is hidden by another element on top of it during processing and when done, the overlay is removed and the element can be clicked. Unfortunately the condition elementIsVisible does not consider an element to be hidden by another element nor does the method WebElement.isDisplayed of an element.
// find an element that is hidden behind some overlay
const hiddenElement = await driver.findElement(By.id('hiddenElement'));
// wait for element returns the "hidden" element
const await visibleElement = driver.wait(webdriver.until.elementIsVisible(hiddenElement));
// "isDisplayed" reports the "hidden" element as visible
const visible = await hiddenElement.isDisplayed();
I could clearly use the overlay element to detect if the element is hidden but this would have to be customized for each different type of overlay and I'm actually looking for a more generic way to detect if an element is actually clickable.

I did actually find a solution that works as expected by myself that can be distilled to the following:
A function isElementClickable that is responsible for checking if an element is clickable:
function isElementClickable(element) {
const SCRIPT = `
const element = arguments[0];
// is element visible by styles
const styles = window.getComputedStyle(element);
if (!(styles.visibility !== 'hidden' && styles.display !== 'none')) {
return false;
}
// is the element behind another element
const boundingRect = element.getBoundingClientRect();
// adjust coordinates to get more accurate results
const left = boundingRect.left + 1;
const right = boundingRect.right - 1;
const top = boundingRect.top + 1;
const bottom = boundingRect.bottom - 1;
if (document.elementFromPoint(left, top) !== element ||
document.elementFromPoint(right, top) !== element ||
document.elementFromPoint(left, bottom) !== element ||
document.elementFromPoint(right, bottom) !== element) {
return false;
}
return true;
`;
return element.getDriver().executeScript(SCRIPT, element);
}
A function 'elementIsClickableCondition' that can be used as a replacement condition instead of webdriver.until.elementIsVisible:
function elementIsClickableCondition(locator) {
return new webdriver.WebElementCondition('until element is visible', async function (driver) {
try {
// find the element(s)
const elements = await driver.findElements(locator);
if (elements.length > 1) {
throw new Error(`elementIsClickableCondition: the locator "${locator.toString()} identifies "${elements.length} instead of 1 element`);
} else if (elements.length < 1) {
return null;
}
const element = elements[0];
// basic check if the element is visible using the build-in functionality
if (!await element.isDisplayed()) {
return null;
}
// really check if the element is visible
const visible = await isElementClickable(element);
return visible ? element : null;
} catch (err) {
if (err instanceof webdriver.error.StaleElementReferenceError) {
return null;
} else {
throw err;
}
}
});
}

Related

Adding a class to a created element

The parameter of my function is a function. It should create an element but I should still be able to add attributes by using the parameter details.
E.g.:
const addElement = (details) => {
const element = document.createElement('div');
}
addElement(function() {
element.id = 'my-div'; // Not working since element is not defined
});
Well, I have tried to store the element in an object to be able to use it outside of that function.
let element = {};
const displayVideo = (type, details) => {
element = document.createElement(type);
element.width = 200;
element.height = 200;
element.classList.add('my-class'); // <--- THE PROBLEM!
if (details) {
details();
}
document.querySelector('#layer').insertBefore(element, document.querySelector('#el'));
};
displayVideo('VIDEO', function () {
element.controls = true;
});
My element can not be created because of element.classList.add('my-class'); and I don't even get an error message. If I remove that line, it works but I would still like to be able to add a class to that object. How can I do this?
Just pass element into the function. Since you're just editing properties on the object, this won't cause reference vs value errors.
const addElement = (details) => {
const element = document.createElement('div');
if (details) details(element);
return element;
}
const ele = addElement(function(element) {
element.id = 'my-div';
});
console.log(ele);
In this case details could be something like classname.
function element(type, classname) {
var element = document.createElement(type);
if (classname !== undefined) {
element.classList.add(classname);
}
return element;
};
element("div","my-class"); //<div class="my-class"></div>
Of course instead of classname you could use an array or an object and loop through in order to set multiple attributes.
Or you could store the return value of your function in a variable and then add all the attributes:
var myelement = element("div");
myelement.classList.add("my-new-class");
myelement //<div class=​"my-new-class">​</div>​

getElementById returns element but then returns null

So I'm trying to set up some references of some DOM elements in my JS. However, for some reason, they return the correct elements, but then return null shortly after. So i tried wrapping them in an if check to make sure the elements aren't null to prevent this. however, it still seems to be happening.
resizer.js
// Define DOM elements
const resizer = document.getElementById('resizer');
let btnMob;
let btnTablet;
let btnLaptop;
let btnDesktop;
// Check buttons exist and asign to variables if they do
if (document.getElementById('resizer-mob') !== null) {
btnMob = document.getElementById('resizer-mob');
}
if (document.getElementById('resizer-tablet') !== null) {
btnTablet = document.getElementById('resizer-tablet');
}
if (document.getElementById('resizer-laptop') !== null) {
btnLaptop = document.getElementById('resizer-laptop');
}
if (document.getElementById('resizer-desktop') !== null) {
btnDesktop = document.getElementById('resizer-desktop');
}
// Define string constants
const DESKTOP_CLASS = 'resizer--desktop';
const LAPTOP_CLASS = 'resizer--laptop';
const TABLET_CLASS = 'resizer--tablet';
const MOB_CLASS = 'resizer--mob';
// Check elements have been asign correctly
console.log('mob is ', btnMob);
console.log('tablet is ', btnTablet);
console.log('laptop is ', btnLaptop);
console.log('desktop is ', btnDesktop);
// Update resizer to display desktop
btnDesktop.addEventListener('click', () => {
console.log("DESKTOP");
resizer.classList.remove(LAPTOP_CLASS);
resizer.classList.remove(TABLET_CLASS);
resizer.classList.remove(MOB_CLASS);
resizer.classList.add(DESKTOP_CLASS);
});
// Update resizer to display laptop
btnLaptop.addEventListener('click', () => {
console.log("LAPTOP");
resizer.classList.remove(DESKTOP_CLASS);
resizer.classList.remove(TABLET_CLASS);
resizer.classList.remove(MOB_CLASS);
resizer.classList.add(LAPTOP_CLASS);
});
// Update resizer to display tablet
btnTablet.addEventListener('click', () => {
console.log("TABLET");
resizer.classList.remove(DESKTOP_CLASS);
resizer.classList.remove(LAPTOP_CLASS);
resizer.classList.remove(MOB_CLASS);
resizer.classList.add(TABLET_CLASS);
});
// Update resizer to display mobile
btnMob.addEventListener('click', () => {
console.log("MOB");
resizer.classList.remove(DESKTOP_CLASS);
resizer.classList.remove(LAPTOP_CLASS);
resizer.classList.remove(TABLET_CLASS);
resizer.classList.add(MOB_CLASS);
});
Have you tried != instead of !==?
And it returns null or undefined?
If it's undefined you need to use:
typeof(element) != 'undefined'
The code is behaving as expected. The first time you run the code everything works as you haven't deleted any elements. The second time you have, and so some elements return null because they don't exist anymore.

Null ref after switching route in React

I have an interesting bug I can't seem to work out and I hope someone with better React knowledge than me can help me work out.
Basically, I have a component (slider carousel, like a Netflix queue) that is trying to set the visibility of two elements (nav slider buttons for left and right nav) if there is overflow of the underlying dev and/or if the underlying div is at a certain position. My visibility setter method is called when onComponentDidMount, when the position of the underlying div changes, and with an window resize event listener.
It works like expected most of the time, however, I have an edge case where I can resize the window, even after going to a new route, and it will work as expected... BUT if I go a new route again I get an error when resizing the window at that point.
It appear as if the refs are not being set after switching routes the second time because they return null.
I've tried detecting if ref is null, but couldn't get that work properly.
setCaretVis() {
const el = this.tray.current;
console.log(el);
const parent = this.wrapper.current;
console.log(parent);
const posRight = this.offsetRight();
const posLeft = el.scrollLeft;
const left = this.caretLeft.current;
const right = this.caretRight.current;
const parWidth = el.parentElement.offsetWidth;
const width = el.scrollWidth;
if (parWidth >= width) {
if (!left.classList.contains("invis")) {
left.classList.add("invis");
} else if (left.classList.contains("invis")) {
}
if (!right.classList.contains("invis")) {
right.classList.add("invis");
}
} else if (parWidth < width) {
if (left.classList.contains("invis") && posLeft != 0) {
left.classList.remove("invis");
} else if (!left.classList.contains("invis") && posLeft === 0) {
left.classList.add("invis");
}
if (right.classList.contains("invis") && posRight != 0) {
right.classList.remove("invis");
} else if (!right.classList.contains("invis") && posRight === 0) {
right.classList.add("invis");
}
}
if (posLeft > 0) {
left.classList.remove("invis");
} else {
left.classList.add("invis");
}
if (posRight === 0) {
console.log("true");
right.classList.add("invis");
} else {
right.classList.remove("invis");
}
}
offsetRight() {
const el = this.tray.current;
//const element = this.refs.tray;
const parent = this.wrapper.current;
const parWidth = parent.offsetWidth;
const width = el.scrollWidth;
const left = el.scrollLeft;
let sub = width - parWidth;
let calc = Math.abs(left - sub);
return calc;
};
// The componentDidMount method
componentDidMount() {
this.setCaretVis();
window.addEventListener("resize", this.setCaretVis);
this.setCaretVis();
}
I would like to set the visibility (adding/removing a css class) on resize after route change without error.
current error reads: Uncaught TypeError: Cannot read property 'offsetWidth' of null
I suspect that your component is recreated when you go to a new route again, but the old listener is still invoked by the resize handler. Try to remove event listener in componentWillUnmount:
componentDidMount() {
this.setCaretVis();
window.addEventListener("resize", this.setCaretVis);
this.setCaretVis();
}
componentWillUnmount() {
window.removeEventListener("resize", this.setCaretVis);
}
When router recreates the component, it will subscribe to resize event again.
From the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any DOM elements that were created in componentDidMount

How can I check that an element is visible with Puppeteer and pure JavaScript?

I wish to check that a DOM element is visible with Puppeteer and pure JavaScript (not jQuery), how can I do this? By visible I mean that the element is displayed through CSS, and not hidden (f.ex. by display: none).
For example, I can determine whether my element #menu is not hidden via CSS rule display: none, in the following way:
const isNotHidden = await page.$eval('#menu', (elem) => {
return elem.style.display !== 'none'
})
How can I determine in general though if the element is hidden or not, and not just through display: none?
I found that Puppeteer has an API method for this purpose: Page.waitForSelector, via its visible option. I wasn't aware of the latter option, but it lets you wait until an element is visible.
await page.waitForSelector('#element', {
visible: true,
})
Conversely you can wait for an element to be hidden, via the hidden option.
I think this is the idiomatic answer, with regards to the Puppeteer API. Thanks to Colin Cline though as I think his answer is probably useful as a general JavaScript solution.
One is by checking its display style value.
Second is by checking its height, for exp if the element is a child of an element which is display: none, the offsetHeight will be 0 and thus you know the element is not visible despite its display value. opacity: 0 is not considered as hidden element so we will not checking it.
const isNotHidden = await page.$eval('#menu', (elem) => {
return window.getComputedStyle(elem).getPropertyValue('display') !== 'none' && elem.offsetHeight
});
You can check elem.offsetWidth as well and is not bad before any calculation, check if element exist or not.
Use boundingBox()
This method returns the bounding box of the element (relative to the main frame), or null if the element is not visible.
API: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#elementhandleboundingbox
The current accepted answer involves waiting for an element to appear and become visible.
If we are not interested in waiting on the element, and we would simply like to test the visibility of the element, we can use a combination of getComputedStyle() and getBoundingClientRect() to test whether or not the element is visible.
We can first check that the visibility is not set to hidden.
Then we can validate that the bounding box is visible by checking that the bottom, top, height, and width attributes are not set to 0 (this will filter out elements that have display set to none as well).
const element_is_visible = await page.evaluate(() => {
const element = document.querySelector('#example');
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.visibility !== 'hidden' && !!(rect.bottom || rect.top || rect.height || rect.width);
});
Maybe you can using elementHandle.boundingBox() (thank to #huypham idea)
It will return a Promise that show a bounding box of the element (relative to the main frame), or null if the element is not visible.
The snippet example:
const loadMoreButton = await getDataPage.$(
'button.ao-tour-reviews__load-more-cta.js-ao-tour-reviews__load-more-cta'
);
const buttonVisible = await loadMoreButton.boundingBox();
if (buttonVisible) {
await loadMoreButton.click().catch((e) => {
console.log('💥💥💥: ' + e)
});
}
The answer #aknuds1 gave is perfect but you may make it even more convenient for yourself by creating a helper such as this one. This resolves to true if the element is visible and to false otherwise.
function isElementVisible(page, selector, timeout = 150) {
return new Promise((resolve) => {
page.waitForSelector(selector, {visible: true, timeout}).then(() => {
resolve(true);
}).catch(() => {
resolve(false);
});
});
}
Usage
in pure JS with Puppeteer
let isVisible = await isElementVisible(page, selector)
isVisible = await isElementVisible(page, selector, 300)
and if you happen to use Jest or another framework
expect(await isElementVisible(page, selector)).toBeTrue();
in the case of Jest (and most other frameworks), you can go even further and create a custom matcher to extend the existing ones. (https://jestjs.io/docs/expect#expectextendmatchers)
expect.extend({
async toHaveVisible(page, selector, timeout = 150) {
let isVisible = await isElementVisible(page, selector, timeout);
if (isVisible) {
return {
message: () => `expected ${selector} not to be visible`,
pass: true
};
} else {
return {
message: () => `expected ${selector} to be visible`,
pass: false
};
}
}
});
await expect(page).toHaveVisible(selector);
await expect(page).not.toHaveVisible(anotherSelector);
await expect(page).not.toHaveVisible(yetAnotherSelector, 300);
based on playwright's logic for checking if the element is visible - https://github.com/microsoft/playwright/blob/master/src/server/injected/injectedScript.ts#L120-L129
function isVisible(element: Element): boolean {
// Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return true;
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
Apparently here's how jQuery does it:
visible = await page.evaluate((e) => e.offsetWidth > 0 && e.offsetHeight > 0, element)
If you just want to know if an element is visible or not then you can use this function. You should make sure that the page is ready before calling this function. You can do that by using waitForSelector on other elements you expect to be visible.
async function isVisible(page, selector) {
return await page.evaluate((selector) => {
var e = document.querySelector(selector);
if (e) {
var style = window.getComputedStyle(e);
return style && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}
else {
return false;
}
}, selector);
}
// Example usage:
page.waitForSelector('#otherPeerElement');
var myElementIsVisible = await isVisible(page, '#visibleOrNot');
if (myElementIsVisible) {
// Interact with #visibleOrNot
}
this code definitely help you.
It basically means the element is already available on the page but is not visible yet or in CSS, the display property is set as none or visibility is hidden. Now, while writing our tests, we assume that as soon as the element is available, do an action on it like clicking or typing. But as this element is not yet visible, Puppeteer fails to perform that action.
async function isLocatorReady(element, page) {
const isVisibleHandle = await page.evaluateHandle((e) =>
{
const style = window.getComputedStyle(e);
return (style && style.display !== 'none' &&
style.visibility !== 'hidden' && style.opacity !== '0');
}, element);
var visible = await isVisibleHandle.jsonValue();
const box = await element.boxModel();
if (visible && box) {
return true;
}
return false;
}
const firstName= await page.$('[name=firstName]')
expect(firstName!=null).equal(true)
I would use #aknuds1 's approach, but you can also do the following.
expect((await page.$('#element')) !== null).toEqual(true)
If you are fetching a resource asynchronously, be aware that the above expectation may not pass, since it won't wait for the changes to reflect on the UI. That's why this approach may not be preferred in this scenario.

JavaScript equivalent to angularjs ng-if

If I want to remove/add element on DOM I just use ng-if and the code under it does not compile into to DOM, can I do the same using pure js? I don't want the HTML code inside my js code.
Hiding it using CSS:
<div id = "infoPage" style="display: none;">
Will still insert the element to the DOM.
EDIT
The condition for showing or not is based on a flag like:
var show = false; //or true
You can try something like this:
Idea:
Create an object that holds reference of currentElement and its parent (so you know where to add).
Create a clone of current element as you want to add same element after its removed.
Create a property using Object.defineProperty. This way you can have your own setter and you can observe changes over it.
To toggle element, check
If value is true, you have to add element. But check if same element is already available or not to avoid duplication.
If false, remove element.
var CustomNGIf = function(element, callback, propertyName) {
var _value = null;
// Create copies of elements do that you can store/use it in future
this.parent = element.parentNode;
this.element = element;
this.clone = null;
// Create a property that is supposed to be watched
Object.defineProperty(this, propertyName, {
get: function() {
return _value;
},
set: function(value) {
// If same value is passed, do nothing.
if (_value === value) return;
_value = !!value;
this.handleChange(_value);
}
});
this.handleChange = function(value) {
this.clone = this.element.cloneNode(true);
if (_value) {
var index = Array.from(this.parent.children).indexOf(this.element);
// Check if element is already existing or not.
// This can happen if some code breaks before deleting node.
if (index >= 0) return;
this.element = this.clone.cloneNode(true);
this.parent.appendChild(this.element);
} else {
this.element.remove();
}
// For any special handling
callback && callback();
}
}
var div = document.getElementById('infoPage');
const propName = 'value';
var obj = new CustomNGIf(div, function() {
console.log("change")
}, propName);
var count = 0;
var interval = setInterval(function() {
obj[propName] = count++ % 2;
if (count >= 10) {
window.clearInterval(interval);
}
}, 2000)
<div class='content'>
<div id="infoPage"> test </div>
</div>

Categories