# How `fill`, `press`, and `insertText` Are Implemented in ego-lite: A Deep Dive into Keyboard Automation

> Explore how ego-lite implements keyboard automation using CDP key events and DOM manipulation. Understand the distinct event patterns for fill, press, and insertText.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-08-01

---

**ego-lite implements keyboard automation through Chrome DevTools Protocol (CDP) key events, element resolution, and DOM manipulation scripts, with `fill`, `press`, and `type` each following distinct event-dispatching patterns.**

The **ego-lite** browser automation framework provides Playwright-style keyboard interactions that translate high-level JavaScript calls into precise CDP commands. Understanding these implementations reveals how the library achieves reliable cross-platform input simulation without native OS dependencies.

## Architecture Overview: From JavaScript Call to Browser Event

All keyboard operations in ego-lite follow a three-layer architecture:

1. **Selector resolution** via `waitForSelector`
2. **Element handle acquisition** via `withHandle`
3. **Event dispatch** via `browserCdp.send('Input.dispatchKeyEvent', …)` or evaluated scripts

This design ensures that operations fail gracefully when elements aren't ready and that events are indistinguishable from genuine user input.

## Core Keyboard Implementation in [`keyboard.ts`](https://github.com/citrolabs/ego-lite/blob/main/keyboard.ts)

The primary implementation resides in [`/src/driver/keyboard.ts`](https://github.com/citrolabs/ego-lite/blob/main//src/driver/keyboard.ts) (citrolabs/ego-lite). This module exports a `Keyboard` class with methods for every interaction type.

### Key Definitions and Modifier Tracking

The keyboard module maintains internal state for modifier keys:

```typescript
// src/driver/keyboard.ts
private pressedModifiers: Set<string> = new Set();

private activeModifierBits(additional?: string): number {
  // Combines currently pressed modifiers with any new ones
  const mods = new Set(this.pressedModifiers);
  if (additional) mods.add(additional);
  return [...mods].reduce((bits, mod) => bits | MODIFIER_BITS[mod], 0);
}

```

**Key definitions** map semantic names to CDP-compatible event properties. The `KEYS` constant (line ~80-150) defines special keys like **Enter**, **Tab**, **Backspace**, and **Escape** with their `key`, `code`, and native virtual key codes.

The `parseKeyCombo` function splits compound shortcuts:

```typescript
// Parses "Control+Shift+a" into modifiers and key
parseKeyCombo('Control+a') → { modifiers: ['Control'], key: 'a' }
parseKeyCombo('Shift+Tab') → { modifiers: ['Shift'], key: 'Tab' }

```

## How `press` Works: Single Keystroke Dispatch

The `press` method is the atomic building block for all typing operations.

### Implementation Flow

```typescript
// src/driver/keyboard.ts
async press(keyCombo: string, options: { delay?: number } = {}): Promise<void> {
  await this.down(keyCombo);
  if (options.delay) await sleep(options.delay);
  await this.up(keyCombo);
}

```

**`down(keyCombo)`** performs:

1. Calls `parseKeyCombo` to extract modifiers and target key
2. Looks up the key in `KEYS` or treats it as a literal character
3. Constructs a CDP `Input.dispatchKeyEvent` payload with:
   - `type: 'keyDown'`
   - `code`, `key`, `windowsVirtualKeyCode`, `nativeVirtualKeyCode`
   - `modifiers`: bitwise OR of active modifier flags
4. Sends via `this.cdp.send('Input.dispatchKeyEvent', payload)`
5. If the key is a modifier (Control, Alt, Shift, Meta), adds to `pressedModifiers`

**`up(keyCombo)`** mirrors this with `type: 'keyUp'` and removes modifiers from the tracking set.

Example usage:

```javascript
// Press Escape to close a modal
await keyboard.press('Escape');

// Press Ctrl+S with 100ms hold time
await keyboard.press('Control+s', { delay: 100 });

```

## How `fill` Works: Direct Value Injection with Event Simulation

The `fill` method takes a different approach than `press`—it uses JavaScript evaluation for value assignment rather than individual keystrokes, making it significantly faster for large text blocks.

### Element Acquisition Pipeline

```typescript
// src/driver/keyboard.ts
async fill(
  selector: string,
  value: string,
  options: { clearFirst?: boolean } = {}
): Promise<void> {
  // 1. Wait for element with smart polling
  const element = await waitForSelector(this.page, selector);
  
  // 2. Execute within element handle
  await withHandle(element, async (handle) => {
    // 3. Injection script runs in page context
    return this.cdp.evaluate(/* script */, [handle, value, options.clearFirst]);
  });
}

```

### The Injection Script (Simplified)

The actual script evaluates in the browser and handles three element types:

```javascript
// Executed via browser-runtime.ts CDP evaluation
function fillElement(element, value, clearFirst) {
  // Validate editability
  const isEditable = element.isContentEditable ||
    element.tagName === 'INPUT' ||
    element.tagName === 'TEXTAREA';
  if (!isEditable) throw new Error('Element is not editable');
  
  // Clear existing content
  if (clearFirst !== false) {
    element.focus();
    element.select?.();
    element.value = ''; // or textContent for contentEditable
    
    // Dispatch 'deleteContentBackward' input event for realism
    element.dispatchEvent(new InputEvent('input', {
      inputType: 'deleteContentBackward',
      bubbles: true
    }));
  }
  
  // Insert new value
  if (element.isContentEditable) {
    element.textContent = value;
  } else {
    element.value = value;
  }
  
  // Critical: dispatch insertText event for framework reactivity
  element.dispatchEvent(new InputEvent('input', {
    inputType: 'insertText',
    data: value,
    bubbles: true
  }));
  
  // Trigger change/blur for form validation
  element.dispatchEvent(new Event('change', { bubbles: true }));
}

```

**Key design decisions in `fill`:**

- **Uses CDP evaluation**, not `dispatchKeyEvent`, for speed
- **Always fires `input` events** with proper `inputType` so React/Vue/Angular bindings detect changes
- **Handles both `<input>`/`<textarea>` and `contentEditable`** through branch logic
- **Optional `clearFirst`** allows appending rather than replacing

## How `insertText` / `type` Works: Character-by-Character Simulation

The `type` method (often aliased as `insertText` in documentation) provides the most realistic input simulation by generating individual keystroke events for each character.

### Implementation

```typescript
// src/driver/keyboard.ts
async type(
  selector: string,
  text: string,
  options: { delay?: number } = {}
): Promise<void> {
  // Resolve element first
  await waitForSelector(this.page, selector);
  
  // Iterate through each code point
  for (const char of text) {
    // Check for special key aliases (e.g., "Enter" in string)
    if (KEYS[char]) {
      await this.press(char, options);
    } else {
      // Dispatch as literal character
      await this.dispatchCharacter(char, options.delay);
    }
  }
}

private async dispatchCharacter(char: string, delay?: number): Promise<void> {
  // Direct CDP dispatch without modifier parsing
  await this.cdp.send('Input.dispatchKeyEvent', {
    type: 'keyDown',
    text: char,           // Unicode character
    unmodifiedText: char, // Without modifiers
    key: char,
    code: `Key${char.toUpperCase()}`,
    // Virtual key codes computed from char.codePointAt(0)
  });
  
  if (delay) await sleep(delay);
  
  await this.cdp.send('Input.dispatchKeyEvent', {
    type: 'keyUp',
    key: char
  });
}

```

Critical differences from `press`:

- **No modifier parsing**—each character is treated as literal input
- **`text` field populated** in CDP payload, which signals "this is a text input event"
- **Preserves natural typing rhythm** through per-character delays

Example:

```javascript
// Simulate realistic typing with human-like pacing
await keyboard.type('#tweet', 'Just shipped ego-lite v1.0! 🚀', {
  delay: 50 // 50ms between keystrokes
});

```

## Supporting Infrastructure: Waits and Element Operations

### `waitForSelector` (src/driver/waits.ts)

Before any interaction, ego-lite ensures element presence:

```typescript
// Implements exponential backoff polling
async function waitForSelector(
  page: Page,
  selector: string,
  options: { timeout?: number; visible?: boolean } = {}
): Promise<ElementHandle> {
  const deadline = Date.now() + (options.timeout ?? 30000);
  
  while (Date.now() < deadline) {
    const element = await page.querySelector(selector);
    if (element && (!options.visible || await element.isVisible())) {
      return element;
    }
    await sleep(Math.min(100, deadline - Date.now()));
  }
  throw new TimeoutError(`Selector "${selector}" not found`);
}

```

### `withHandle` (src/driver/element-ops.ts)

Provides safe CDP handle management:

```typescript
async function withHandle<T>(
  element: ElementHandle,
  callback: (handle: Runtime.RemoteObjectId) => Promise<T>
): Promise<T> {
  const handle = await element.getBackendNodeId();
  try {
    return await callback(handle);
  } finally {
    // Always release to prevent memory leaks
    await element.dispose();
  }
}

```

## CDP Transport Layer (browser-runtime.ts)

All CDP commands flow through `BrowserRuntime` in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts):

```typescript
// Low-level CDP sender used by keyboard operations
class BrowserRuntime {
  async send(method: string, params: object): Promise<any> {
    const id = ++this.messageId;
    this.ws.send(JSON.stringify({ id, method, params }));
    return this.waitForResponse(id);
  }
  
  async evaluate(script: string, args: any[]): Promise<any> {
    // Uses Runtime.callFunctionOn or Runtime.evaluate
    // depending on whether args contain remote object handles
  }
}

```

The `Input.dispatchKeyEvent` domain is documented in the Chrome DevTools Protocol specification and accepts parameters matching the W3C UI Events model.

## Comparison: When to Use Each Method

| Method | Speed | Realism | Best For |
|--------|-------|---------|----------|
| **`fill`** | Fastest (single round-trip) | Low (no key events) | Form population, bulk data entry |
| **`type`** | Slow (per-character) | Highest | Testing input validation, keystroke listeners |
| **`press`** | Fast | Medium | Shortcuts, navigation, single special keys |

## Common Patterns and Edge Cases

### Content-Editable Elements

ego-lite detects `contentEditable` and switches injection strategy:

```javascript
// From fill implementation
const isContentEditable = await this.cdp.evaluate(
  'el => el.isContentEditable',
  [handle]
);

```

### Shadow DOM Piercing

When elements reside in shadow roots, `waitForSelector` uses Chrome's `>>>` piercing combinator internally, falling back to JavaScript traversal if CSS piercing is disabled.

### Frame/Target Context

Keyboard operations respect the `page` context they're bound to. Multi-frame scenarios require obtaining a `Keyboard` instance from the specific frame's `Page` object.

## Summary

- **`press`** dispatches individual `keyDown`/`keyUp` CDP events through `dispatchKeyEvent`, honoring modifier state via `pressedModifiers` and `activeModifierBits`
- **`fill`** evaluates JavaScript in page context to directly set values, firing synthetic `input` events with proper `insertType` for framework compatibility
- **`type`** iterates characters, calling `dispatchCharacter` for each to generate realistic keystroke sequences with configurable delays
- All methods rely on `waitForSelector` (src/driver/waits.ts) for element resolution and `withHandle` (src/driver/element-ops.ts) for safe CDP handle management
- The CDP transport in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) bridges high-level calls to Chrome's `Input.dispatchKeyEvent` domain

## Frequently Asked Questions

### Does ego-lite use real OS-level key events or browser-only events?

ego-lite uses **browser-only CDP events**. The `Input.dispatchKeyEvent` Chrome DevTools Protocol command injects events directly into the browser's input queue without involving the operating system. This provides cross-platform consistency but means the target window need not be focused—useful for headless automation.

### Why does `fill` not trigger `keydown`/`keyup` listeners?

`fill` uses JavaScript evaluation to set values directly, bypassing the key event lifecycle entirely. If your application logic depends on `keydown`, `keypress`, or `keyup` handlers, use `type` instead. The trade-off is speed: `fill` completes in one CDP round-trip versus potentially hundreds for long strings with `type`.

### How are emoji and non-BMP characters handled?

ego-lite processes strings as JavaScript iterables, correctly handling surrogate pairs. The `dispatchCharacter` method passes Unicode code points directly through CDP's `text` parameter, which Chrome converts to appropriate `compositionstart`/`compositionend` events for IME compatibility.

### Can I combine modifiers with `type` for shifted characters?

No—`type` sends literal characters without modifier simulation. For shifted symbols (e.g., `@` from `Shift+2`), either pass the literal character to `type` or use `press('Shift+2')` explicitly. The `press` method's `parseKeyCombo` correctly translates these into combined modifier and key events.