← Back to Index

Lesson 7

The Input Domain

What You'll Learn

The Input domain lets you synthesise user interactions - clicks, typing, scrolling, touch gestures - at the browser's input pipeline level. This is what Puppeteer's page.click() and page.type() call under the hood.

For a CDP adapter, this is the "action" side: the DiagnosticCollector observes, and the Input domain acts.

Two Approaches to Clicking

There are two ways to trigger a click via CDP:

1. Input.dispatchMouseEvent - simulate real mouse input

This sends actual mouse events through the browser's input pipeline. The page sees mousedown, mouseup, and click events exactly as if a real user clicked:

// A click requires three dispatches: mousePressed, mouseReleased
// (the browser generates the 'click' event from the pair)

async function click(x, y) {
  await send('Input.dispatchMouseEvent', {
    type: 'mousePressed',
    x,
    y,
    button: 'left',
    clickCount: 1
  });

  await send('Input.dispatchMouseEvent', {
    type: 'mouseReleased',
    x,
    y,
    button: 'left',
    clickCount: 1
  });
}

You need pixel coordinates. To click on a specific element, you first need to find its position (we'll cover that below).

2. Runtime.evaluate - call element.click() directly

await send('Runtime.evaluate', {
  expression: 'document.querySelector("[data-testid=\\"submit-btn\\"]").click()'
});

This is simpler but less realistic - it doesn't simulate mouse movement, doesn't trigger mouseenter/mouseleave events, and bypasses any pointer-event CSS restrictions. Some frameworks rely on the full event sequence.

Which should a CDP adapter use? For most journey steps (clicking buttons, selecting options), element.click() via Runtime.evaluate is simpler and more reliable - you don't need to calculate coordinates. Use Input.dispatchMouseEvent when you need realistic input simulation (testing hover states, drag-and-drop, or when click() doesn't work due to event delegation patterns).

Getting Element Coordinates

If you do need pixel coordinates (for dispatchMouseEvent), use DOM.getBoxModel:

// Find the element
const doc = await send('DOM.getDocument', { depth: 0 });
const buttonNode = await send('DOM.querySelector', {
  nodeId: doc.root.nodeId,
  selector: '[data-testid="submit-btn"]'
});

// Get its position and size
const box = await send('DOM.getBoxModel', {
  nodeId: buttonNode.nodeId
});

// box.model.content is an array of 8 numbers: [x1,y1, x2,y2, x3,y3, x4,y4]
// representing the four corners of the content box
const [x1, y1, x2, y2, x3, y3, x4, y4] = box.model.content;

// Calculate the center point
const centerX = (x1 + x3) / 2;
const centerY = (y1 + y3) / 2;

// Now click at the center
await click(centerX, centerY);

Keyboard Input

There are three ways to put text into an input field via CDP. They differ significantly in what browser events they generate - which matters for Angular forms.

Option 1: Input.dispatchKeyEvent - full keystroke simulation

This simulates the complete keyboard event sequence for each character. For the letter "a", the browser generates:

  1. keydown (key pressed down)
  2. keypress (character is about to be inserted - deprecated but still fired)
  3. The character is inserted into the focused element's value
  4. input event fires (value changed)
  5. keyup (key released)

This is the most realistic simulation - identical to a real user pressing keys one at a time. Angular's form listeners see every input event, one per character.

// Type "Hello" character by character - full event sequence per character
async function typeTextRealistic(text) {
  for (const char of text) {
    await send('Input.dispatchKeyEvent', {
      type: 'char',
      text: char
    });
  }
}

// The 'char' type is a shortcut that combines keyDown + character insertion + keyUp
// For full fidelity you'd send keyDown then char then keyUp separately, but
// 'char' alone is sufficient for most inputs.

When to use: When you need per-character events (e.g., autocomplete/typeahead that reacts to each keystroke), or when testing debounced input handlers that fire on individual key events.

Option 2: Input.insertText - bulk insertion via the input method

This inserts the entire text at once, as if the user pasted it or used an input method editor (IME). It goes through the browser's "text input" pipeline but does NOT fire individual keydown/keyup events.

What the browser generates:

  1. A single beforeinput event (with inputType: 'insertText')
  2. The entire text is inserted into the focused element's value at once
  3. A single input event fires (value changed)

No keydown, no keyup, no keypress. Just beforeinput → text inserted → input.

// Insert "Hello, World!" in one shot - one input event, not 13
await send('Input.insertText', {
  text: 'Hello, World!'
});

Crucially: The input event still fires. This means Angular's form listeners detect the change - the model updates correctly. You get the benefit of speed (one CDP call instead of a loop) without the Angular forms gotcha.

When to use: Most of the time. It's fast (single CDP command), fires the input event Angular needs, and is what Puppeteer uses internally for page.type() in fast mode.

Option 3: element.value = 'x' via Runtime.evaluate - direct property mutation

This bypasses the browser's input pipeline entirely. It's just JavaScript mutating a DOM property.

What the browser generates: nothing. No events at all.

// Sets the value but fires NO events - Angular won't see the change
await send('Runtime.evaluate', {
  expression: `document.querySelector('#email').value = 'test@example.com'`
});

This is the approach that breaks Angular forms. You'd need to manually dispatch events afterward:

// Fix: manually fire the events Angular listens for
await send('Runtime.evaluate', {
  expression: `
    const el = document.querySelector('#email');
    el.value = 'test@example.com';
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
  `
});

When to use: Rarely. Only when you need to set a value without simulating user interaction (e.g., pre-filling a hidden field), or when the above two approaches don't work for a specific edge case.

Summary comparison

Approach Fires keydown/keyup? Fires input event? Angular sees it? Speed
dispatchKeyEvent (per char) Yes Yes (per char) Yes Slow (N calls)
Input.insertText No Yes (once) Yes Fast (1 call)
element.value = x No No No (needs manual fix) Fast (1 call)
Recommendation for a CDP adapter: Use Input.insertText as the default for typing into form fields. It's fast, fires the right events, and Angular stays happy. Only fall back to per-character dispatchKeyEvent when testing typeahead/autocomplete behaviour that depends on individual keystrokes.

Special keys

For keys like Enter, Tab, Escape, you need keyDown and keyUp events with the appropriate key code (there's no insertText equivalent for non-character keys):

// Press Enter
await send('Input.dispatchKeyEvent', {
  type: 'keyDown',
  key: 'Enter',
  code: 'Enter',
  windowsVirtualKeyCode: 13,
  nativeVirtualKeyCode: 13
});
await send('Input.dispatchKeyEvent', {
  type: 'keyUp',
  key: 'Enter',
  code: 'Enter',
  windowsVirtualKeyCode: 13,
  nativeVirtualKeyCode: 13
});

// Press Tab
await send('Input.dispatchKeyEvent', {
  type: 'keyDown',
  key: 'Tab',
  code: 'Tab',
  windowsVirtualKeyCode: 9,
  nativeVirtualKeyCode: 9
});
await send('Input.dispatchKeyEvent', {
  type: 'keyUp',
  key: 'Tab',
  code: 'Tab',
  windowsVirtualKeyCode: 9,
  nativeVirtualKeyCode: 9
});

Modifier keys (Ctrl, Shift, Alt)

For keyboard shortcuts, set the modifiers bitmask:

// Ctrl+A (select all)
// Modifiers: Alt=1, Ctrl=2, Meta/Command=4, Shift=8
await send('Input.dispatchKeyEvent', {
  type: 'keyDown',
  key: 'a',
  code: 'KeyA',
  modifiers: 2,  // Ctrl
  windowsVirtualKeyCode: 65
});
await send('Input.dispatchKeyEvent', {
  type: 'keyUp',
  key: 'a',
  code: 'KeyA',
  modifiers: 2,
  windowsVirtualKeyCode: 65
});
Modifier bitmask values: Alt = 1, Ctrl = 2, Meta/Command = 4, Shift = 8. Combine with bitwise OR: Ctrl+Shift = 2 | 8 = 10.

Practical Adapter Helpers

Combining the approaches above, here's what a CDP adapter's action methods would look like in practice:

// Click - simplest approach for a CDP adapter
async function clickElement(selector) {
  await send('Runtime.evaluate', {
    expression: `document.querySelector('${selector}').click()`
  });
}

// Type into a focused input - insertText fires the 'input' event Angular needs
async function typeInto(selector, text) {
  // Focus the element first
  await send('Runtime.evaluate', {
    expression: `document.querySelector('${selector}').focus()`
  });

  // Insert the text - fires beforeinput + input events, Angular model updates
  await send('Input.insertText', { text });
}

// Clear and type (common pattern for form inputs)
async function clearAndType(selector, text) {
  // Focus and select all existing content
  await send('Runtime.evaluate', {
    expression: `
      const el = document.querySelector('${selector}');
      el.focus();
      el.select();
    `
  });

  // insertText replaces the selection - fires input event
  await send('Input.insertText', { text });
}

// Select a dropdown option
async function selectOption(selector, value) {
  await send('Runtime.evaluate', {
    expression: `
      const select = document.querySelector('${selector}');
      select.value = '${value}';
      select.dispatchEvent(new Event('change', { bubbles: true }));
    `
  });
}
Angular forms note: Angular's reactive forms and template-driven forms listen for input and change events to update their models. When setting values programmatically, you need to dispatch these events manually (as shown above) or Angular won't know the value changed. This is a common gotcha in automation.

Scrolling

Scroll events are dispatched via Input.dispatchMouseEvent with the mouseWheel type:

// Scroll down by 500 pixels
await send('Input.dispatchMouseEvent', {
  type: 'mouseWheel',
  x: 400,          // position in viewport
  y: 300,
  deltaX: 0,
  deltaY: 500      // positive = scroll down
});

// Or scroll an element into view programmatically (simpler)
await send('Runtime.evaluate', {
  expression: `document.querySelector('${selector}').scrollIntoView({ behavior: 'smooth' })`
});

Touch Events

For testing mobile interactions, use Input.dispatchTouchEvent:

// Tap at coordinates
await send('Input.dispatchTouchEvent', {
  type: 'touchStart',
  touchPoints: [{ x: 200, y: 300 }]
});
await send('Input.dispatchTouchEvent', {
  type: 'touchEnd',
  touchPoints: []
});

// Swipe (touch start, move, end)
await send('Input.dispatchTouchEvent', {
  type: 'touchStart',
  touchPoints: [{ x: 200, y: 500 }]
});
await send('Input.dispatchTouchEvent', {
  type: 'touchMove',
  touchPoints: [{ x: 200, y: 200 }]  // swipe up
});
await send('Input.dispatchTouchEvent', {
  type: 'touchEnd',
  touchPoints: []
});

Key Commands Reference

Command Purpose
Input.dispatchMouseEvent Simulate mouse actions (click, move, wheel scroll)
Input.dispatchKeyEvent Simulate individual key presses (keyDown, keyUp, char)
Input.insertText Bulk insert text (like paste - no individual key events)
Input.dispatchTouchEvent Simulate touch interactions (tap, swipe)
DOM.getBoxModel Get element position (needed for coordinate-based clicks)

For the full list, see the Input section of the complete reference.

Primary Source

Read the Input domain documentation. Pay attention to the dispatchMouseEvent type values (mousePressed, mouseReleased, mouseMoved, mouseWheel) and the key event types (keyDown, keyUp, char, rawKeyDown).