Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Input

Every input method on Element comes in two variants:

  • Realistic (default) — Bezier-interpolated cursor moves for the mouse, per-character delays with occasional typos for the keyboard. Tuned to defeat behavioral fingerprinters.
  • _fast — single CDP dispatch, no delays, no jitter, no typos. Skips the actionability gate. For tests and fast automation flows where deterministic timing matters more than realism.

Both flavors route through the same shared InputController on each tab, so the OS-level modifier state (Shift, Ctrl, etc.) stays consistent across realistic and fast paths.

Realistic vs _fast

#![allow(unused)]
fn main() {
async fn ex() -> zendriver::Result<()> {
let browser = zendriver::Browser::builder().launch().await?;
let tab = browser.main_tab();
let btn = tab.find().css("button").one().await?;

// Realistic: Bezier-path cursor approach, hover, then mousedown/up.
btn.click().await?;

// Fast: single Input.dispatchMouseEvent, no actionability gate.
btn.click_fast().await?;
Ok(()) }
}
MethodCursor pathGateUse case
click()BezieractionabilityDefault. Indistinguishable.
click_fast()teleportskippedTests; trusted automation.
hover()BezieractionabilityDefault. Real cursor approach.
hover_fast()teleportskippedTests; trusted automation.
type_text(s)per-char + delaysfocus gateDefault. Sub-keystroke timing.
type_text_fast(s)per-char, no delayfocus gateTests; trusted automation.

The realism comes from the active StealthProfile's InputProfile:

  • StealthProfile::native() and ::spoofed() install a realistic profile by default — Bezier control points with deterministic-but- jittered timing, per-character keyboard delays of 30-200 ms, occasional 1-2% typo + correction events.
  • StealthProfile::off() installs a no-op profile — even realistic methods just do the dispatch without realism.

When realism matters but you also want determinism (e.g. snapshots inside tests), seed the profile with a fixed RNG — see the InputProfile rustdoc.

ClickOptions for fine control

Both click() and click_fast() are wrappers around Element::click_with(), which takes a ClickOptions struct for full control:

#![allow(unused)]
fn main() {
use zendriver::{ClickOptions, MouseButton, KeyModifiers};

async fn ex() -> zendriver::Result<()> {
let browser = zendriver::Browser::builder().launch().await?;
let tab = browser.main_tab();
let row = tab.find().css("tr.contact").one().await?;

// Right-click.
row.click_with(ClickOptions {
    button: MouseButton::Right,
    ..Default::default()
}).await?;

// Ctrl+click (open in new tab).
let link = tab.find().css("a.external").one().await?;
link.click_with(ClickOptions {
    modifiers: KeyModifiers::CTRL,
    ..Default::default()
}).await?;

// Double-click.
let item = tab.find().css(".item").one().await?;
item.click_with(ClickOptions {
    click_count: 2,
    ..Default::default()
}).await?;

// Click at a specific offset inside the element's bbox.
let canvas = tab.find().css("canvas").one().await?;
canvas.click_with(ClickOptions {
    position: Some((100.0, 50.0)),
    ..Default::default()
}).await?;
Ok(()) }
}

The full ClickOptions shape:

FieldTypeDefaultMeaning
buttonMouseButtonMouseButton::LeftWhich button to dispatch.
modifiersKeyModifiersKeyModifiers::empty()Modifier bits held during dispatch.
click_countu321clickCount for the dispatch (2 = double-click).
forceboolfalseSkip the actionability gate. Mirrors Playwright.
realisticbooltrueBezier path vs teleport.
positionOption<(f64, f64)>None (bbox center)Click offset relative to bbox top-left.

Keyboard: Key, KeyModifiers, SpecialKey

For single-key dispatches (Enter, Tab, arrow keys, Ctrl+A, etc.):

#![allow(unused)]
fn main() {
use zendriver::{Key, KeyModifiers, SpecialKey};

async fn ex() -> zendriver::Result<()> {
let browser = zendriver::Browser::builder().launch().await?;
let tab = browser.main_tab();
let input = tab.find().css("input").one().await?;

// Press Enter (named special key).
input.press(Key::Special(SpecialKey::Enter)).await?;

// Press Tab to move focus.
input.press(Key::Special(SpecialKey::Tab)).await?;

// Ctrl+A (select all).
input.press_with(Key::Char('a'), KeyModifiers::CTRL).await?;

// Ctrl+Shift+End.
input.press_with(
    Key::Special(SpecialKey::End),
    KeyModifiers::CTRL | KeyModifiers::SHIFT,
).await?;
Ok(()) }
}

Key is one of:

  • Key::Char(char) — any typeable character.
  • Key::Special(SpecialKey) — named non-character key.

SpecialKey covers Enter, Tab, Escape, Backspace, Delete, Space, all four arrows, Home, End, PageUp, PageDown, F1-F12, Insert, CapsLock, NumLock, ScrollLock, PrintScreen, Pause, ContextMenu — the full non-character keyboard.

KeyModifiers is a bitflags struct:

  • KeyModifiers::ALT — Alt (Option on macOS).
  • KeyModifiers::CTRL — Control.
  • KeyModifiers::META — Meta (Command on macOS, Windows key on Windows).
  • KeyModifiers::SHIFT — Shift.

Combine with |: KeyModifiers::CTRL | KeyModifiers::SHIFT.

press vs press_with

  • Element::press(key) uses whatever modifiers are currently held by the InputController — useful when you've explicitly tracked modifier-held state (e.g. via held-key sequences).
  • Element::press_with(key, mods) passes mods straight through to the CDP dispatch for this one call, without mutating the controller's tracked state — the safer default when you want a single key event with specific modifiers.

End-to-end form fill

//! Fill out a small HTML form rendered via `data:` URL — demonstrates the
//! P3 input surface end-to-end without depending on any third-party site.
//!
//! Equivalent in spirit to the form-fill snippets scattered through the
//! Python `examples/` directory (`network_monitor.py`'s search-and-submit
//! flow, `imgur_upload_image.py`'s title-field fill). Picks `data:` over
//! a third-party form so the example stays deterministic across runs.
//!
//! Sequence:
//!   1. CSS-select the inputs and submit button.
//!   2. [`Element::type_text`] simulates per-character key events with the
//!      Bezier/jitter realism from the [`StealthProfile`]'s `InputProfile`.
//!   3. [`Element::click`] dispatches a real `mousedown` + `mouseup` via
//!      `Input.dispatchMouseEvent` after running the actionability gates.
//!   4. Read back the form's serialized state via `evaluate_main` to prove
//!      the inputs took our values.

use zendriver::Browser;

const FORM_HTML: &str = "data:text/html,\
<!doctype html><html><body>\
<form id='f' onsubmit='window.submitted=true;return false'>\
<input id='user' name='user' />\
<input id='pass' name='pass' type='password' />\
<button id='go' type='submit'>Submit</button>\
</form></body></html>";

#[tokio::main]
#[allow(clippy::result_large_err)] // example boundary; users wrap in their own Error
async fn main() -> zendriver::Result<()> {
    tracing_subscriber::fmt::init();

    let browser = Browser::builder().headless(true).launch().await?;
    let tab = browser.main_tab();
    tab.goto(FORM_HTML).await?;
    tab.wait_for_load().await?;

    let user = tab.find().css("#user").one().await?;
    user.type_text("rin").await?;

    let pass = tab.find().css("#pass").one().await?;
    pass.type_text("hunter2").await?;

    let go = tab.find().css("#go").one().await?;
    go.click().await?;

    let user_val: String = tab
        .evaluate_main("document.getElementById('user').value")
        .await?;
    let submitted: bool = tab.evaluate_main("window.submitted === true").await?;
    println!("user field = {user_val:?}, submitted = {submitted}");

    browser.close().await?;
    Ok(())
}

Expected output:

user field = "rin", submitted = true

The example demonstrates the realistic input surface end-to-end: per-character typing into two inputs, then a single click on the submit button. All three calls (type_text + type_text + click) go through the actionability gate and the realistic-cursor path.

When to use _fast variants

  • Tests where you only care that the action happened, not how it looked to a fingerprinter.
  • Trusted automation pipelines (internal admin tools, scraping flows where you already know stealth isn't being checked).
  • CI where every saved millisecond per click compounds across thousands of runs.
  • Setup steps (typing a known query into a search box before the real interaction starts). Save realism for the moments that matter.

When in doubt, stick with the realistic defaults — the per-call cost is small (typically 50-300 ms per click; a few ms per typed character) and it keeps you on the "indistinguishable from a real user" path that the rest of the stealth machinery is built around.