In Selenium, what does calling perform() on an Actions chain send, and how are its ticks ordered?
answer
- Nothing leaves your process while chaining
- One sequence per virtual input device
- A column across all the sequences
- Single POST to the session actions endpoint
- Tick costs the longest duration, not the sum
basics
~20 sActions collects each step into a per-device sequence, and perform() posts them all as one WebDriver actions command. Steps sharing a tick run together across devices, and the browser waits for that tick's longest duration before the next one.
solid answer
~40 sAn `Actions` builder in Selenium 4 does not talk to the browser while you chain calls. Each convenience method appends interactions to the sequence belonging to its input source - a `PointerInput`, a `KeyInput` or a `WheelInput`. A tick is one column across those sequences: when you add an action for one source, `Actions.tick` pads every other known source with a zero-length `Pause`, so the sequences stay aligned. `perform()` calls `build()` and sends the whole collection as a single POST to `/session/{session id}/actions`. The remote end runs it tick by tick, dispatching everything in a tick together, then waiting out that tick's duration - the longest `pause`, `pointerMove` or `scroll` in it - before moving on. `build()` also clears the builder, so a second `perform()` on the same object sends nothing.
go deeper
Be ready to say that Actions only collects steps and that perform() is what sends them. Knowing a forgotten perform() fails silently is worth more here than knowing the wire format.
Explain the per-device sequences, the tick as a column across them, and the zero-length pauses Selenium pads in to keep them aligned. Name the single actions request the whole chain becomes.
Show you use the shape deliberately: split a chain around an assertion, tune move durations for a page that animates, and explain why one request means no mid-gesture check is possible.
Own the tradeoff between a hand-built pointer sequence and a simpler command. A sequence encodes timing your team must keep working across browsers, window sizes and machine speeds.
## What an Actions chain is before it runs `Actions` in Selenium 4 is a **builder**, not a command channel. On an expense-report approval list, writing `new Actions(driver).moveToElement(reportRow).contextClick()` sends nothing to the browser. Each call appends **interactions** to an internal map keyed by **input source**, and that map only becomes a command when you finish the chain. That is why a chain you forget to `perform()` is a silent no-op: there is no request, so there is nothing to fail. Selenium models three kinds of input source, and every convenience method belongs to exactly one of them: - **`PointerInput`** - a virtual mouse, pen or touch pointer. `moveToElement`, `moveByOffset`, `clickAndHold`, `release`, `contextClick`, `doubleClick`, `dragAndDrop` and `dragAndDropBy` all write here. - **`KeyInput`** - a virtual keyboard. `keyDown`, `keyUp` and the chain's own `sendKeys` write here. - **`WheelInput`** - a virtual scroll wheel. `scrollToElement`, `scrollByAmount` and `scrollFromOrigin` write here. If you never name a device, `Actions` lazily creates one called `"default mouse"`, `"default keyboard"` or `"default wheel"` the first time it needs it. `setActivePointer(PointerInput.Kind.MOUSE, name)`, `setActiveKeyboard(name)` and `setActiveWheel(name)` let you drive more than one device of the same type. ## What a tick is A **tick** is one column across all the per-source sequences: a slice of time in which every input source does at most one thing. `Actions.tick(...)` enforces two rules that shape everything else. 1. **One action per source per tick.** Handing two interactions from the same source to a single tick throws `IllegalStateException` with the message that you may only add one action per input source per tick. 2. **Every other source is padded.** After your action is appended, `tick` adds a zero-length `Pause` to every sequence that did not act, so all sequences stay the same length and stay aligned. That padding is why a chain reads sequentially even though the wire format is parallel. Selecting two expense rows with `keyDown(Keys.CONTROL).click(rowA).click(rowB).keyUp(Keys.CONTROL)` puts the key press in tick 1 while the mouse pauses; `click(rowA)` takes ticks 2 and 3 on the mouse (a `pointerDown` then a `pointerUp`) while the keyboard pauses; and so on. ## What perform() actually sends `perform()` calls `build()` and hands the collected sequences to the driver, which issues **one** `POST` to `/session/{session id}/actions`. Two consequences matter every day: - The whole gesture is **a single round trip**. Hovering a report row, holding the button down, gliding to the approved bucket and releasing costs one request, not four. - `build()` **clears the builder** as it produces the composite action. Calling `perform()` twice on the same `Actions` object therefore performs the chain once and then sends an empty set. Re-chain the steps, or keep the `Action` that `build()` returned and perform that. ## How the remote end times each tick The driver runs the sequences tick by tick. For each tick it computes a **tick duration**, dispatches every action in that tick, then waits until the generated DOM events have been processed and at least the tick duration has elapsed. | Action in the tick | Contributes a duration? | |---|---| | `pause` | Yes - the pause length | | `pointerMove` | Yes - the move duration | | `scroll` (wheel) | Yes - the scroll duration | | `pointerDown` / `pointerUp` | No | | `keyDown` / `keyUp` | No | The tick duration is the **maximum** of the contributing durations, not their sum, so a one-second `pause` alongside a 250 ms `pointerMove` in the same tick costs one second. Selenium's own defaults feed straight into this: `moveToElement` emits a 100 ms move, `moveByOffset` 200 ms, `moveToLocation` 250 ms, and the wheel actions use the duration given to the `Actions` constructor, which defaults to 250 ms and can be changed with `new Actions(driver, Duration.ofMillis(50))`. ## Why the shape is worth knowing - A gesture that must look continuous to the page - press, glide, release - is expressed as **durations inside the sequence**, so the driver paces it rather than your test code. - Because it is one request, you cannot assert anything **between** the steps. If the approval list must be inspected mid-drag, split the chain: `clickAndHold(row).moveToElement(bucket).perform()`, assert, then `release().perform()`. - Because the sequences are per source, genuinely simultaneous input is expressible - pause one device while another acts, then swap. - `pause(Duration)` inserts a real wait **inside** the sequence. It is a fixed delay used to pace a gesture, and it is unrelated to Selenium's wait mechanisms, which are a separate subject.
- Why does calling perform() twice on the same Actions object run the chain only once?`perform()` delegates to `build()`, which snapshots the collected sequences into a composite action and then clears the builder's own map. The first call performs the real chain; the second finds an empty set of sequences and sends an effectively empty actions command. Rebuild the chain, or hold the `Action` that `build()` returned and perform that object instead.
- How would you make the pointer and the keyboard act in the same tick?Create the devices yourself with `setActivePointer` and `setActiveKeyboard`, build one interaction from each, and hand both to `Actions.tick(...)` in a single call. The convenience methods always add one interaction per tick and pad the other sources, so on their own they cannot express two devices acting simultaneously.
- Where does the pointer start if a chain's first step is moveByOffset?From the pointer's current position in the driver's input state for that browsing context, which persists across `Actions` objects and across earlier commands in the session; if nothing has moved it, it starts at the viewport origin. `moveToElement` is unaffected, because it uses an element origin rather than the pointer's own position.
saying these in an interview costs you the question
- Thinks each chained call sends its own request to the browser
- Believes perform() can be called twice to repeat the same chain
- Adds Thread.sleep between chain calls instead of pause inside the sequence
- Assumes a tick costs the sum of its durations rather than the maximum
- Expects a chain that is never performed to fail loudly