After a Ctrl+click Actions chain in Selenium, later steps in the same session misbehave. Why, and what clears it?
answer
- The browser remembers, not your builder
- Modifiers are never released implicitly
- A new builder object changes nothing
- Pair every press with its release
- A DELETE on the session actions endpoint
basics
~20 sSelenium's Actions never releases a modifier for you. A keyDown with no matching keyUp leaves the key depressed in the driver's input state, which outlives the chain and any new Actions object. Release it with keyUp, sendKeys(Keys.NULL), or resetInputState.
solid answer
~40 sIn Selenium 4 the depressed keys and buttons live in the **remote end's** input state for the top-level browsing context, not in the `Actions` builder. `keyDown(Keys.CONTROL)` is documented as never releasing the key implicitly, so a chain that ends without `keyUp` leaves `CONTROL` held for the rest of the session; every later click arrives as a Ctrl+click and every later `sendKeys` as shortcuts. Constructing a fresh `Actions` does not help - it clears your local sequences, not the browser's state. Fix it at the source by pairing each `keyDown` with a `keyUp` (or `sendKeys(Keys.NULL)`) in the same chain, and pair `clickAndHold` with `release`. To recover, call `((RemoteWebDriver) driver).resetInputState()`, which sends the protocol's Release Actions command and genuinely fires the undo events.
code
java · 13 linesActions actions = new Actions(driver);
WebElement first = driver.findElement(By.cssSelector("[data-report-id='EXP-4471']"));
WebElement second = driver.findElement(By.cssSelector("[data-report-id='EXP-4472']"));
actions.keyDown(Keys.CONTROL)
.click(first)
.click(second)
.keyUp(Keys.CONTROL)
.perform();
((RemoteWebDriver) driver).resetInputState();
new Actions(driver).click(first).perform();go deeper
Remember that a modifier you press with keyDown stays pressed until you release it. Always write the matching keyUp in the same chain, right after the clicks it applies to.
Explain where the state lives: the driver's input state per browsing context, not the builder. Name keyUp, sendKeys with the NULL key, and resetInputState as the three ways to release.
Demonstrate the diagnosis. A failure that moves with test ordering and passes in isolation points at leaked input state, and teardown-level recovery is what stops it spreading through a shared session.
Own the policy question: whether drivers are shared across tests at all decides whether this class of leak can exist, and that trades run time against isolation for the whole suite.
## The state the driver keeps for you A WebDriver session does not forget which keys and buttons are down. The W3C WebDriver protocol that Selenium 4 speaks gives every top-level browsing context an **input state**: a map of the virtual input sources that have been used, plus an **input cancel list** recording, for every `keyDown` dispatched, the matching `keyUp` that would undo it, and for every `pointerDown`, the matching `pointerUp`. The pointer's current position lives there too. `Actions.keyDown(CharSequence)` is documented as pressing a modifier and **never** releasing it implicitly. Nothing in the driver, the session or the test framework tidies up after a chain ends. So an expense-report suite that selects rows with ```java new Actions(driver).keyDown(Keys.CONTROL).click(rowA).click(rowB).perform(); ``` - no `keyUp` - leaves `CONTROL` depressed for the rest of the session. The next test's click on an approval button arrives as a Ctrl+click, the next `sendKeys("2000")` into an amount field can arrive as a run of keyboard shortcuts, and the failures land in whichever test happens to run next rather than in the one that caused them. `clickAndHold` with no `release` behaves the same way with the left mouse button, and it is worse, because a held button makes every later pointer move look like a drag. ## A fresh Actions object does not help The most expensive misconception here is that the state belongs to the builder. It does not. `new Actions(driver)` creates an empty local sequence map, and `build()` clears that map, but neither touches the **remote end's** input state. Constructing a new `Actions` after a leaked modifier gives you a clean chain that is still typed against a browser holding `CONTROL` down. ## The three ways to let go | Mechanism | Where it runs | What it clears | |---|---|---| | `keyUp(Keys.CONTROL)` in the same chain | A step in your sequence | Just that key | | `sendKeys(Keys.NULL)` on the chain | A step in your sequence | The modifiers the chain pressed | | `((RemoteWebDriver) driver).resetInputState()` | A separate command | Every depressed key and button, and the device state | The first two are the ones to reach for by default: a chain that presses a modifier should release it, in the same chain, before `perform()`. `Actions`' own documentation names both `keyUp(theKey)` and `sendKeys(Keys.NULL)` as the way to release a modifier. `resetInputState()` is the recovery hatch. It comes from the `Interactive` interface that `RemoteWebDriver` implements, and it is **not** part of a chain - it does not go through `perform()`, which is why the call sites look different from everything else in this API: ```java ((RemoteWebDriver) driver).resetInputState(); ``` ## What Release Actions actually does `resetInputState()` issues `DELETE /session/{session id}/actions`, the protocol's **Release Actions** command. It does not simply forget the state: 1. It takes the input cancel list and reverses it. 2. It dispatches those undo actions, so the page really receives the `keyup` and `pointerup` events it was waiting for. 3. It then discards the input state for that top-level browsing context, resetting the virtual devices. Step 2 is the part people miss. Because the events are genuinely fired, a page that keeps its own "is a modifier held" flag - a multi-select approval list very often does - is corrected rather than left inconsistent with the browser. ## Diagnosing it in a suite The signature is a failure that **moves**. The test that leaks the modifier passes; a later, unrelated test fails, and it passes on its own or when run first. Ordering changes the victim, which is why this is often mistaken for an environment problem. - Check whether any test uses `keyDown` or `clickAndHold` without a matching `keyUp` or `release` on **every** path, including the path where an assertion throws mid-chain. - Look for chains that end early because an exception was raised between `clickAndHold` and `release`; the sequence sent so far still executed, so the button is still down. - Remember that a driver reused across tests carries the state; a driver created per test does not, because the session is new. ## Habits that keep it out - Pair every `keyDown` with a `keyUp` in the same chain, and every `clickAndHold` with a `release` in the same chain, so the pairing is visible in one expression. - Where a chain can be abandoned by a failure, call `resetInputState()` in the teardown that runs after a failing test, so a leak cannot outlive the test that created it. - Treat a bare `keyDown` at the end of a chain as a defect in review, the way an unclosed resource would be.
- What does resetInputState actually do beyond forgetting the state?It issues `DELETE /session/{session id}/actions`, the Release Actions command. The remote end replays the input cancel list in reverse, so the page really receives the matching `keyup` and `pointerup` events, and only then discards the input state for that browsing context. A page tracking its own modifier flag is therefore corrected, not left inconsistent.
- Why does this defect usually surface in a different test from the one that caused it?The leaking chain itself completes successfully; the held modifier only distorts the next interaction in the same session. So the failure lands in whichever test runs next against that driver, and it disappears when that test runs alone or first. Ordering changes the victim, which makes it look like an environment problem.
- An assertion throws between clickAndHold and release. What state is the browser left in?The steps already performed still ran, so if the chain was split across several `perform()` calls the left button can be left down. Every later pointer move then reads as a drag. Calling `resetInputState()` in the teardown that runs after a failing test stops the leak outliving the test.
It is like taping the Ctrl key down on a shared keyboard: the next person's typing looks perfectly ordinary to them, but the machine keeps reading everything as a shortcut until somebody lifts the tape.
saying these in an interview costs you the question
- Thinks a new Actions object resets the browser's input state
- Believes the modifier is released automatically when the chain ends
- Blames flaky infrastructure for a failure that moves with test ordering
- Calls resetInputState as a chain step ending in perform()
- Leaves clickAndHold without a matching release on the failure path