How Keyboard Input Actions Work in ego-browser: fill, press, and pressSequentially Explained
fill, press, and pressSequentially in ego-browser are Chrome DevTools Protocol (CDP) wrappers that handle key definition, modifier parsing, and fallback DOM event synthesis to guarantee reliable input delivery.
Keyboard automation in ego-browser follows a predictable pipeline: logical keys are decoded into virtual-key codes, modifiers are tracked across calls, and every dispatch is verified with a probe-and-fallback mechanism. This article examines the source code in package/ego-browser/src/driver/keyboard.ts to show exactly how each action functions.
Where Keyboard Actions Are Implemented
All keyboard helpers live in package/ego-browser/src/driver/keyboard.ts. The file exports three main user-facing functions—fill, press, and pressSequentially—plus internal utilities for key parsing, modifier management, and CDP event dispatching.
The implementation depends on supporting modules:
element-ops.ts— provideswithHandleandresolveAndCallfor element resolutionwaits.ts— supplieswaitForSelectorfor timeout handlingstate.ts— contains default timeouts and thesleeputility
How Keys Are Defined and Translated
Before any event reaches the browser, key names must be converted into CDP-compatible payloads. The keyDefinition() function handles this transformation.
// Simplified logic from keyboard.ts lines 49-62
function keyDefinition(key: string) {
// Returns: { vk, code, text } for CDP Input.dispatchKeyEvent
}
For printable characters, the regex PRINTABLE_CODE_RE maps values like "a" to "KeyA" and "7" to "Digit7". Non-printable keys (e.g., "Enter", "Tab") use predefined virtual-key codes without text content.
This translation layer ensures that Playwright-style key names work consistently across platforms.
Parsing Modifier Key Combinations
The parseKeyCombo() utility accepts strings like "Control+a" or "Shift+Tab" and decomposes them into a base key plus a modifier bitfield.
// From keyboard.ts lines 100-130
function parseKeyCombo(combo: string): {
modifiers: number; // Bitfield from MODIFIER_BITS
key: string; // Base key without modifiers
}
Unrecognized modifiers throw errors immediately. The MODIFIER_BITS constant defines standard values for Control, Shift, Alt, and Meta.
Tracking Active Modifiers State
Keyboard actions are stateful. The module maintains a Set called pressedModifiers that records which modifier keys are currently held down.
// keyboard.ts lines 98-106
const pressedModifiers = new Set<string>();
activeModifierBits() merges these into a single integer so subsequent calls preserve context. For example, holding Shift during one press call and pressing "Tab" in the next correctly produces a Shift+Tab event.
The Core Dispatch Pipeline: dispatchKeyEvent
Every key event flows through dispatchKeyEvent(), a thin wrapper around CDP's Input.dispatchKeyEvent method.
// keyboard.ts lines 30-37
async function dispatchKeyEvent(event: KeyEvent): Promise<void> {
// Calls CDP with INPUT_DISPATCH_TIMEOUT_MS protection
}
A timeout guard prevents hanging calls when the browser becomes unresponsive.
Fallback Probing: Guaranteed Delivery
CDP dispatch can be ignored by certain pages (event listeners calling preventDefault(), shadow DOM boundaries, etc.). To handle this, ego-browser implements a probe-and-fallback system:
| Stage | Function | Purpose |
|---|---|---|
| 1. Install probe | installKeyProbe() |
Injects a temporary page-side listener that marks keys as "seen" |
| 2. Dispatch | dispatchKeyEvent() |
Sends the CDP event |
| 3. Verify & fallback | finishKeyProbe() |
Checks probe status; if missed, synthesizes DOM KeyboardEvent |
// Probe installation: keyboard.ts lines 39-45
function installKeyProbe(key: string): void
// Probe completion with fallback: lines 63-70
function finishKeyProbe(): Promise<void>
This guarantees that inputs reach the page even when CDP fails.
press: Single Key or Combination
The press() action sends one key event (with optional modifiers) and follows the complete pipeline.
Execution flow:
- Parse combo → base key + modifiers via
parseKeyCombo() - Compute effective modifier bits:
activeModifierBits() | modifiers - Build base event object with
keyEventBase() - Send
keyDownwith printabletextand editing commands - Wait
INPUT_EVENT_DELAY_MS(tiny inter-event delay) - Send matching
keyUp - Execute
installKeyProbe→finishKeyProbefallback routine
// Simple key press
await press('Enter');
// Modifier combination
await press('Control+a');
The full implementation spans keyboard.ts lines 99-44.
pressSequentially: Character-by-Character Input
pressSequentially() types a string one character at a time, with optional per-character delays.
Two calling signatures:
// Focus-first variant
pressSequentially(selector: string, text: string, options?: { delay?: number })
// Direct typing variant
pressSequentially(text: string, options?: { delay?: number })
Internal process:
- If selector provided, call
focusWithTimeout()to activate the element - Iterate over each character in
text - For each character:
await press(char) - Respect
options.delayviastate.sleep()between keystrokes
See implementation at keyboard.ts lines 35-57.
// Type with 50ms between keystrokes
await pressSequentially('#comment', 'Nice post!', { delay: 50 });
// Focus and type immediately
await pressSequentially('#username', 'alice');
fill: Direct Value Injection
fill() sets an input's value directly using CDP's Input.insertText, bypassing per-character key events entirely.
Execution steps:
- Optional timeout →
waitForSelector() - Resolve element handle via
withHandle() - Focus element; if
clearFirst(defaulttrue), select all existing text - When clearing: page script removes value and fires
inputevent withinputType: "deleteContentBackward" - Call CDP
Input.insertTextwith the new value - Fire final
inputandchangeevents to simulate user completion
// Default: clear existing value, then fill
await fill('#search-box', 'Hello World');
// Preserve existing content (append instead of replace)
await fill('#notes', 'Additional text', { clearFirst: false });
Core implementation: keyboard.ts lines 74-86 with CDP steps at lines 88-124.
Comparing the Three Keyboard Actions
| Action | Use Case | CDP Method | Event Type | Clear Existing? |
|---|---|---|---|---|
| press | Single keys, shortcuts | dispatchKeyEvent |
keyDown/keyUp |
No |
| pressSequentially | Mimic human typing | dispatchKeyEvent × N |
Character-by-character | Optional focus |
| fill | Fast value insertion | insertText |
input/change |
Yes (default) |
Choose fill for speed and reliability on standard inputs. Use pressSequentially when event handlers depend on individual keystroke timing. Reserve press for navigation keys and modifier shortcuts.
Working Example: Complete Form Interaction
// Navigate to a page and complete a form
await fill('#email', 'user@example.com');
await fill('#password', 'secret123');
await press('Tab'); // Move to submit button
await press('Enter'); // Activate
// Alternative: slower, more human-like entry
await pressSequentially('#otp', '123456', { delay: 100 });
All helpers are available on globalThis.ego in agent scripts.
Summary
- Key definition in
keyDefinition()maps logical names to CDP-compatible codes usingPRINTABLE_CODE_RE - Modifier parsing via
parseKeyCombo()supports Playwright-style combinations with bitfield tracking - State management through
pressedModifiersSet andactiveModifierBits()maintains context across calls - Guaranteed delivery using
installKeyProbe()andfinishKeyProbe()with DOM fallback synthesis - press() dispatches individual
keyDown/keyUppairs through the full probe-enabled pipeline - pressSequentially() iterates characters with optional delays, calling
press()per character - fill() uses
Input.insertTextfor direct value injection, firing properinputandchangeevents
Frequently Asked Questions
What happens if a page blocks CDP keyboard events?
finishKeyProbe() detects when the probe signal is missed and automatically synthesizes a native DOM KeyboardEvent as a fallback. This ensures the input reaches the page regardless of preventDefault() handlers or shadow DOM boundaries.
How do I hold a modifier key across multiple press calls?
Modifiers are tracked in the pressedModifiers Set. Call press() with a modifier key to add it, then issue subsequent presses. The activeModifierBits() function automatically includes held modifiers in each event's modifier bitfield.
Why would I use pressSequentially instead of fill?
Use pressSequentially when the page's JavaScript expects individual keystroke events—common in autocomplete fields, character-counting inputs, or security-sensitive forms that validate during typing. fill bypasses per-character events and may trigger fewer handlers.
Can I combine clearFirst: false with fill for appending text?
Yes. Pass { clearFirst: false } in the options parameter. The element receives focus and the new text is inserted via Input.insertText without clearing existing content. Note that this appends rather than replaces; precise cursor positioning requires additional handling.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →