Actions Class — Hover, Drag-Drop, Right-Click, Double-Click

9 min read

element.click() and element.sendKeys(...) cover the boring 80% of interactions. The other 20% — hovering to reveal a menu, dragging a card to a new column, right-clicking to open a context menu, holding Shift to select a range — needs the Actions class. Actions is Selenium's interaction builder. You compose a sequence of mouse and keyboard events, then call .perform() to execute them as a single user gesture. This lesson covers every method you'll actually use, plus the most-asked question of every Selenium engineer's career: "why doesn't drag-and-drop work on this React app?"

The Actions class — build, then perform

Actions lives in org.openqa.selenium.interactions. Construct one with the driver, chain method calls, then call perform():

import org.openqa.selenium.interactions.Actions;
 
Actions actions = new Actions(driver);
 
actions.moveToElement(menu)
       .pause(Duration.ofMillis(300))
       .click(submenuItem)
       .perform();

The single most-forgotten method on this class is perform(). Without it, the chain builds up but never fires — the test silently does nothing. If you ever see "the test doesn't fail, but the action didn't happen," perform() is the first thing to check.

Hover — moveToElement

Hover-to-reveal menus are the most common reason to reach for Actions. Cypress and Playwright support real hover too; in Selenium it's moveToElement:

WebElement topNav = driver.findElement(By.cssSelector("[data-testid='nav-products']"));
new Actions(driver).moveToElement(topNav).perform();
 
// The submenu is now visible — interact with it
WebElement settingsLink = wait.until(
    ExpectedConditions.elementToBeClickable(By.linkText("Settings"))
);
settingsLink.click();

Note that hover doesn't keep itself active across calls. Once moveToElement perform()s, the cursor stays where it is — but if your next action moves elsewhere, you'll lose the hover and the menu closes. Either chain the menu click in the same Actions builder, or wait inside the same hover-active window.

Click variations — double, right, click-and-hold

Actions actions = new Actions(driver);
 
// Double-click — opens an editable field, opens a file in some apps
actions.doubleClick(driver.findElement(By.id("editable-cell"))).perform();
 
// Right-click — opens the browser-context-aware menu, or a custom context menu
actions.contextClick(driver.findElement(By.id("file-icon"))).perform();
 
// Click and hold (without releasing) — useful as a building block
actions.clickAndHold(driver.findElement(By.id("slider-handle"))).perform();
 
// Release at a specific element
actions.moveToElement(driver.findElement(By.id("track-end"))).release().perform();

contextClick opens whatever context menu the page wires up — most modern apps replace the browser's default menu with a custom one, and you click items in that custom menu like any other element.

Drag and drop — the simple case

WebElement source = driver.findElement(By.id("draggable"));
WebElement target = driver.findElement(By.id("droppable"));
 
new Actions(driver).dragAndDrop(source, target).perform();
 
// Or — by pixel offset
new Actions(driver).dragAndDropBy(source, 200, 50).perform();

dragAndDrop is shorthand for clickAndHold(source).moveToElement(target).release(). On a page that uses native HTML drag-and-drop events (mouse events, no dragstart/dragover), this works.

Drag and drop — the hard case (HTML5 DnD)

Here's the famous Selenium pitfall. Actions.dragAndDrop simulates a mouse-down → move → mouse-up sequence. It does not dispatch the HTML5 drag-and-drop event family (dragstart, dragover, drop) with a populated DataTransfer object. Many React/Vue/Angular DnD libraries (react-dnd, react-beautiful-dnd, sortablejs in some configurations) listen only to those events. Selenium's Actions does nothing visible.

The widely-used workaround is a JavaScript snippet injected via JavascriptExecutor that fires the proper events with a synthetic DataTransfer:

import org.openqa.selenium.JavascriptExecutor;
 
// Pre-baked simulator (search "selenium html5 drag drop simulator" — many gists exist)
String DND_SCRIPT =
    "function createEvent(type) {" +
    "  var event = new DragEvent(type, { bubbles: true, cancelable: true });" +
    "  return event;" +
    "}" +
    "function dispatch(elem, type, dt) {" +
    "  var event = createEvent(type);" +
    "  Object.defineProperty(event, 'dataTransfer', { value: dt });" +
    "  elem.dispatchEvent(event);" +
    "}" +
    "var src = arguments[0], tgt = arguments[1];" +
    "var dt = new DataTransfer();" +
    "dispatch(src, 'dragstart', dt);" +
    "dispatch(tgt, 'dragenter', dt);" +
    "dispatch(tgt, 'dragover', dt);" +
    "dispatch(tgt, 'drop', dt);" +
    "dispatch(src, 'dragend', dt);";
 
((JavascriptExecutor) driver).executeScript(DND_SCRIPT, source, target);

Many teams compromise by adding a test-only button or keyboard shortcut (e.g., "Move card up") that triggers the same business action without going through DnD events. Cleaner tests, lower flake — at the cost of one developer-side hook.

Keyboard chords with Actions

Actions also handles keyboard combinations that sendKeys alone can't. The pattern: keyDown → action(s) → keyUp:

import org.openqa.selenium.Keys;
 
WebElement input = driver.findElement(By.id("search"));
 
// Ctrl+A then Delete to clear a stubborn rich-text editor
new Actions(driver)
    .click(input)
    .keyDown(Keys.CONTROL).sendKeys("a").keyUp(Keys.CONTROL)
    .sendKeys(Keys.DELETE)
    .perform();
 
// Ctrl+Click to multi-select two items
new Actions(driver)
    .keyDown(Keys.CONTROL)
    .click(driver.findElement(By.id("item-1")))
    .click(driver.findElement(By.id("item-3")))
    .keyUp(Keys.CONTROL)
    .perform();

Keys.COMMAND for macOS-style shortcuts, Keys.CONTROL for Windows/Linux. If your suite runs on both, parameterise on System.getProperty("os.name").

Drag-and-drop, visualised

Step 1 of 5

Find source

driver.findElement(By...) — locate the element being dragged. Wait for clickability if the page is still loading.

A sortable-list test

A complete Actions-based test against a public DnD demo page:

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
import java.time.Duration;
 
public class ActionsTest {
 
    WebDriver driver;
    WebDriverWait wait;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        driver.get("https://practice.expandtesting.com/drag-and-drop");
    }
 
    @Test
    public void shouldDragColumnAToB() {
        WebElement columnA = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("column-a")));
        WebElement columnB = driver.findElement(By.id("column-b"));
 
        new Actions(driver).dragAndDrop(columnA, columnB).perform();
 
        // After the swap, what was in columnA is now where columnB was
        // The exact assertion depends on the demo's behaviour
        Assert.assertNotNull(driver.findElement(By.id("column-a")));
    }
 
    @Test
    public void shouldHoverThenClickSubmenu() {
        driver.get("https://qa.codes");
        WebElement learnNav = wait.until(ExpectedConditions.elementToBeClickable(By.linkText("Learn")));
 
        new Actions(driver).moveToElement(learnNav).perform();
        // Submenu items now visible — interact with them as usual
        Assert.assertTrue(learnNav.isDisplayed());
    }
 
    @AfterMethod
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

How Cypress and Playwright handle this

// Playwright — drag-and-drop with HTML5 DnD support out of the box
await page.locator("#source").dragTo(page.locator("#target"));
 
// Cypress — needs cypress-real-events or a synthetic-event approach for HTML5
cy.get("#source").trigger("dragstart");
cy.get("#target").trigger("dragover").trigger("drop");

Playwright's dragTo handles HTML5 events natively — one of the cleanest wins of the modern frameworks. Cypress requires a plugin or manual events. Selenium needs the Actions class for native DnD and a JS workaround for HTML5 — the most boilerplate of the three for this single feature.

The Selenium tool entry lists every Actions method.

⚠️ Common mistakes

  • Forgetting .perform(). The chain builds up an action sequence without firing it. actions.moveToElement(menu).click(submenu); (no .perform()) does nothing — and there's no warning. The test silently fails to interact, and you spend 20 minutes hunting why. Always end with .perform().
  • Assuming Actions.dragAndDrop works on every drag-and-drop UI. It works on native HTML drag-events. It does NOT work on HTML5-DnD apps that listen for dragstart/dragover/drop with a DataTransfer payload — react-dnd, react-beautiful-dnd, etc. The fix is either a JavaScript-dispatched synthetic drag, or a test-only API endpoint that triggers the underlying business action without going through the DnD machinery.
  • Waiting for the result of a drag with Thread.sleep(500). Drops often animate. Wait on the consequence instead — the new position of the dragged element, the count of items in each column, the API call your app fires after a successful drop. WebDriverWait against a stable post-drop assertion is always more reliable than a fixed sleep.

🎯 Practice task

Build Actions-based interactions on a real page. 30–40 minutes.

  1. Add ActionsTest from this lesson to your project. Run both tests; both should pass (the DnD assertion may need adjusting depending on the demo page's exact behaviour).
  2. Hover for a real menu. Open a website with hover-revealed navigation (Amazon, AWS Console, any major SaaS dashboard). Write a test that hovers a top-level nav item, waits for the submenu to be visible, and clicks an item inside. Run it. If the submenu disappears between the hover and the click, chain them inside one Actions builder.
  3. Right-click test. Visit https://swisnl.github.io/jQuery-contextMenu/demo.html or any demo with a custom context menu. Use actions.contextClick(...) to trigger the menu and assert its first item is displayed.
  4. Multi-select with Ctrl+Click. On any page that supports range/multi-selection (Sauce Demo's product list with checkboxes if you add them, or any list-app demo), use keyDown(Keys.CONTROL) + multiple .click(...) + keyUp(Keys.CONTROL) to select three items at once. Assert the count.
  5. The flaky DnD reality. Visit a site that genuinely uses HTML5 DnD (try https://react-dnd.github.io/react-dnd/examples or Trello's demo). Try Actions.dragAndDrop. Watch it fail silently. Then try the JS-injection workaround from the lesson. Watch it work. This is the canonical Selenium-DnD frustration; experiencing it once teaches you to budget time for it on real projects.
  6. Stretch — keyboard-only navigation. Write a test that fills a form entirely via Actions chains: Tab to move between fields, sendKeys for content, Enter to submit. No .click() calls. This is exactly what an a11y-focused user does — and your test now exercises that path.

Next lesson: JavaScript alerts, confirms, and prompts — the native browser dialogs that live outside the DOM and need their own switchTo().alert() API to handle.

// tip to track lessons you complete and pick up where you left off across devices.