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.
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:
keydown(key pressed down)keypress(character is about to be inserted - deprecated but still fired)- The character is inserted into the focused element's value
inputevent fires (value changed)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:
- A single
beforeinputevent (withinputType: 'insertText') - The entire text is inserted into the focused element's value at once
- A single
inputevent 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) |
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
});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 }));
`
});
}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).