Files
playwriter/extension-kapture/modules/background-keypress.js
Tommy D. Rossi ec02a90812 adding examples
2025-11-13 19:42:44 +01:00

215 lines
7.0 KiB
JavaScript

// Import helper functions from background-commands
import { getFromContentScript, respondWith, respondWithError, attachDebugger } from './background-commands.js';
// Additional helper functions
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Send keypress event
export async function keypress({tabId}, params) {
const { key, selector, xpath, delay = 50 } = params;
if (!key) {
return respondWithError(tabId, 'KEY_REQUIRED', 'Key parameter is required');
}
// Validate delay is within reasonable bounds (0-60000ms = 0-60 seconds)
const keypressDelay = Math.max(0, Math.min(60000, delay));
try {
// First, focus the target element if selector/xpath provided
if (selector || xpath) {
const focusResult = await getFromContentScript(tabId, 'focus', { selector, xpath });
if (focusResult.error) {
return focusResult;
}
}
// Parse key combination to extract key and modifiers
const keyData = parseKeyCombination(key);
// Use attachDebugger helper to manage debugger lifecycle
return await attachDebugger(tabId, async () => {
// Helper function to send key event
const sendKeyEvent = async (type, autoRepeat = false, text = null) => {
const eventData = {
type,
...keyData,
windowsVirtualKeyCode: keyData.keyCode,
nativeVirtualKeyCode: keyData.keyCode
};
if (type === 'keyDown' || type === 'keyUp') {
eventData.autoRepeat = autoRepeat;
}
if (text) {
eventData.text = text;
}
await chrome.debugger.sendCommand({ tabId }, 'Input.dispatchKeyEvent', eventData);
};
// Send keydown event
await sendKeyEvent('keyDown', false);
// If delay > 500ms, simulate key repeat
if (keypressDelay > 500) {
// Calculate number of repeat events based on delay
// Typical repeat rate is ~30ms between events
const repeatInterval = 30;
const repeatCount = Math.floor((keypressDelay - 100) / repeatInterval);
// Wait a bit before starting repeat (typical OS behavior)
await wait(50);
// Send repeated keydown events
for (let i = 0; i < repeatCount; i++) {
await sendKeyEvent('keyDown', true);
// Wait between repeat events
if (i < repeatCount - 1) {
await wait(repeatInterval);
}
}
} else if (keypressDelay > 0) {
// For delays <= 500ms, just wait the specified time
await wait(keypressDelay);
}
// Send keyup event
await sendKeyEvent('keyUp');
// Wait a bit for the page to process the key events
await wait(50);
// Keys that typically cause scrolling need extra time to settle
const scrollKeys = ['PageUp', 'PageDown', 'Space', ' ', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
if (!selector && !xpath && scrollKeys.includes(keyData.key)) {
// Additional delay for scroll to complete when key is pressed globally (not on a specific element)
await wait(100);
}
// Get result including element info if selector was provided
const resultData = {
keyPressed: true,
key: key,
delay: keypressDelay
};
// If this was an auto-repeat key press, include that info
if (keypressDelay > 500) {
resultData.autoRepeat = true;
resultData.repeatCount = Math.floor((keypressDelay - 100) / 30);
}
return await respondWith(tabId, resultData, selector, xpath);
});
} catch (error) {
return respondWithError(tabId, 'KEYPRESS_FAILED', error.message, selector, xpath);
}
}
// Parse key combination and return CDP-compatible key data
function parseKeyCombination(keyCombination) {
let modifiers = 0;
let key = '';
// CDP modifier flags
const CDP_MODIFIERS = {
alt: 1,
ctrl: 2,
meta: 4,
shift: 8
};
// Special key mappings
const KEY_MAPPINGS = {
'enter': { key: 'Enter', code: 'Enter', keyCode: 13 },
'return': { key: 'Enter', code: 'Enter', keyCode: 13 },
'tab': { key: 'Tab', code: 'Tab', keyCode: 9 },
'delete': { key: 'Delete', code: 'Delete', keyCode: 46 },
'backspace': { key: 'Backspace', code: 'Backspace', keyCode: 8 },
'escape': { key: 'Escape', code: 'Escape', keyCode: 27 },
'esc': { key: 'Escape', code: 'Escape', keyCode: 27 },
'space': { key: ' ', code: 'Space', keyCode: 32, text: ' ' },
' ': { key: ' ', code: 'Space', keyCode: 32, text: ' ' },
'arrowup': { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
'arrowdown': { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
'arrowleft': { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
'arrowright': { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
'pageup': { key: 'PageUp', code: 'PageUp', keyCode: 33 },
'pagedown': { key: 'PageDown', code: 'PageDown', keyCode: 34 },
'home': { key: 'Home', code: 'Home', keyCode: 36 },
'end': { key: 'End', code: 'End', keyCode: 35 },
'insert': { key: 'Insert', code: 'Insert', keyCode: 45 },
'f1': { key: 'F1', code: 'F1', keyCode: 112 },
'f2': { key: 'F2', code: 'F2', keyCode: 113 },
'f3': { key: 'F3', code: 'F3', keyCode: 114 },
'f4': { key: 'F4', code: 'F4', keyCode: 115 },
'f5': { key: 'F5', code: 'F5', keyCode: 116 },
'f6': { key: 'F6', code: 'F6', keyCode: 117 },
'f7': { key: 'F7', code: 'F7', keyCode: 118 },
'f8': { key: 'F8', code: 'F8', keyCode: 119 },
'f9': { key: 'F9', code: 'F9', keyCode: 120 },
'f10': { key: 'F10', code: 'F10', keyCode: 121 },
'f11': { key: 'F11', code: 'F11', keyCode: 122 },
'f12': { key: 'F12', code: 'F12', keyCode: 123 }
};
// Parse modifiers from combination
let remaining = keyCombination;
const modifierPatterns = [
{ pattern: /^(Control|Ctrl)\+/i, modifier: 'ctrl' },
{ pattern: /^Shift\+/i, modifier: 'shift' },
{ pattern: /^Alt|Option\+/i, modifier: 'alt' },
{ pattern: /^(Meta|Cmd|Command)\+/i, modifier: 'meta' }
];
// Extract modifiers
let foundModifier = true;
while (foundModifier && remaining) {
foundModifier = false;
for (const { pattern, modifier } of modifierPatterns) {
if (pattern.test(remaining)) {
modifiers |= CDP_MODIFIERS[modifier];
remaining = remaining.replace(pattern, '');
foundModifier = true;
break;
}
}
}
// What's left is the key
key = remaining;
// Look up special key mapping
const lowerKey = key.toLowerCase();
if (KEY_MAPPINGS[lowerKey]) {
return {
...KEY_MAPPINGS[lowerKey],
modifiers,
text: KEY_MAPPINGS[lowerKey].text || undefined
};
}
// For single character keys
if (key.length === 1) {
const keyCode = key.toUpperCase().charCodeAt(0);
return {
key: key,
code: 'Key' + key.toUpperCase(),
keyCode: keyCode,
modifiers,
text: modifiers === 0 ? key : undefined // Only send text for unmodified keys
};
}
// For other keys, try to generate a reasonable code
return {
key: key,
code: key,
keyCode: 0, // Unknown keycode
modifiers
};
}