While I hesitate to add yet another answer to a question that already has 30, there are some very subtle things that go on with browser scrolling and Selenium. The crux of the issue is that scrollIntoView is asynchronous: it simply pushes a scroll update request onto a queue of some kind and then exits before the scrolling actually occurs. So it may be easy to scroll to a specific element, but it is difficult to ensure that scrolling has actually completed before moving on so that you can ensure future calls (like click()) won't "miss" their elements and fail due to the elements still moving around on the page. This is true even when smooth scroll-behavior is turned off.
Option 1:
Simply use the common solution mentioned by others, but make sure to add some arbitrary wait afterward:
((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", element);
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
Adding a wait afterward will give the scrolling time to complete after executeScript returns, but there is no good way to determine how long to wait. If scrolling is not something your script does very often, then set it to something way longer than what you think is needed a forget about it.
Option 2:
If you hate using arbitrary waits to gloss over problems (or if you need this operation to take as little time as possible), then use a more complicated script with executeAsyncScript to ensure scrolling completes before the Selenium script moves on.
The script below uses the 'scrollend' event to detect when scrolling has stopped. However, this is not straight forward because (and this is where it gets complicated) we may need to wait for multiple 'scrollend' events because:
- each ancestor of the element may have a scrollbar, and
- each one of them may or may not need to be scrolled in order to scroll the element into view, and
- each ancestor that gets scrolled in order to bring the element into view will generate its own scrollend event (but only if it actually needed to be scrolled during the process)
So, the number of scrollend events we need to wait for is anywhere between 0 and X, where X is the number of ancestor elements with visible scrollbars.
The script below solves this issue by starting at "element" and walking up the hierarchy looking for any ancestor elements that have a scrollbar (vertical or horizontal). For each one, it scrolls that container by a single pixel, just to make sure each scrollable ancestor element has a pending scroll update. It also keeps a count of the number of scrollable ancestor elements it encountered, so that we know exactly how many scrollend events to expect.
Then element.scrollIntoView() is called, which may or may not add more pending scroll updates in order to scroll the element into view. However, these updates will simply be additional updates for the scrollable ancestor elements that our script already programmatically scrolled by one pixel. This means that the element.scrollIntoView() call will not cause the expected number of scrollend events to increase.
After calling element.scrollIntoView(), the script simply needs to wait for the expected number of scrollend events to occur, and then signal the end of the script.
const element = arguments[0];
// keep track of the function to call to signal the script is complete
const scriptDone = arguments[arguments.length - 1];
let scrollableElementCount = 0;
const nonScrollableStyles = ['visible', 'hidden'];
let currentElement = element;
while(currentElement.parentElement && currentElement != currentElement.parentElement) {
currentElement = currentElement.parentElement;
// check to see if currentElement is scrollable in either the X or Y direction
const style = getComputedStyle(currentElement);
if(currentElement.clientHeight > 0 && currentElement.clientHeight < currentElement.scrollHeight && ! nonScrollableStyles.includes(style.overflowY)) {
// current element is scrollable in the Y direction - scroll it by one pixel
currentElement.scrollTop = (currentElement.scrollTop == 0) ? 1 : currentElement.scrollTop - 1;
scrollableElementCount++;
} else if(currentElement.clientWidth > 0 && currentElement.clientWidth < currentElement.scrollWidth && ! nonScrollableStyles.includes(style.overflowX)) {
// current element is scrollable in the X direction - scroll it by one pixel
currentElement.scrollLeft = (currentElement.scrollLeft == 0) ? 1 : currentElement.scrollLeft - 1;
scrollableElementCount++;
}
}
if(scrollableElementCount > 0) {
// add a scrollEndHandler to ensure one "scrollend" event is received for each scrollable element that is an ancestor of the target element
const scrollEndHandler = function () {
scrollableElementCount--;
if(scrollableElementCount == 0) {
element.ownerDocument.removeEventListener('scrollend', scrollEndHandler, true);
// all of the "scrollend" events are now accounted for
scriptDone();
}
};
element.ownerDocument.addEventListener('scrollend', scrollEndHandler, true);
// finally call scrollIntoView and wait for the scrollEndHandler to account for all the expected "scrollend" events
element.scrollIntoView(true);
} else {
// no need to call scrollIntoView, since there are no scrollbars present anywhere on the page
scriptDone();
}
Take that JavaScript, get it into a Java String and call executeAsyncScript
String scrollScript = """
<multi-line Java String - insert JavaScript from above here!>
""";
((JavascriptExecutor) driver).executeAsyncScript(scrollScript, element);
JavascriptExecutor js = (JavascriptExecutor) driver; js.executeScript("window.scrollBy(0,250)", "”);Actions action = new Actions(driver); action.moveToElement(element).perform();WebElement element = driver.findElement(By.<locator>)); element.sendKeys(Keys.DOWN);