@monoes/monobrowse 1.0.26 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/browser/actions-eval.d.ts +8 -0
- package/dist/src/browser/actions-eval.d.ts.map +1 -0
- package/dist/src/browser/actions-eval.js +72 -0
- package/dist/src/browser/actions-eval.js.map +1 -0
- package/dist/src/browser/actions-form.d.ts +10 -0
- package/dist/src/browser/actions-form.d.ts.map +1 -0
- package/dist/src/browser/actions-form.js +133 -0
- package/dist/src/browser/actions-form.js.map +1 -0
- package/dist/src/browser/actions-keyboard.d.ts +7 -0
- package/dist/src/browser/actions-keyboard.d.ts.map +1 -0
- package/dist/src/browser/actions-keyboard.js +131 -0
- package/dist/src/browser/actions-keyboard.js.map +1 -0
- package/dist/src/browser/actions-mouse.d.ts +11 -0
- package/dist/src/browser/actions-mouse.d.ts.map +1 -0
- package/dist/src/browser/actions-mouse.js +105 -0
- package/dist/src/browser/actions-mouse.js.map +1 -0
- package/dist/src/browser/actions.d.ts +12 -28
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +12 -434
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/browser-discovery.d.ts +26 -0
- package/dist/src/browser/browser-discovery.d.ts.map +1 -0
- package/dist/src/browser/browser-discovery.js +113 -0
- package/dist/src/browser/browser-discovery.js.map +1 -0
- package/dist/src/browser/browser-launch.d.ts +3 -0
- package/dist/src/browser/browser-launch.d.ts.map +1 -0
- package/dist/src/browser/browser-launch.js +324 -0
- package/dist/src/browser/browser-launch.js.map +1 -0
- package/dist/src/browser/browser-lifecycle.d.ts +52 -0
- package/dist/src/browser/browser-lifecycle.d.ts.map +1 -0
- package/dist/src/browser/browser-lifecycle.js +233 -0
- package/dist/src/browser/browser-lifecycle.js.map +1 -0
- package/dist/src/browser/browser-page.d.ts +13 -0
- package/dist/src/browser/browser-page.d.ts.map +1 -0
- package/dist/src/browser/browser-page.js +179 -0
- package/dist/src/browser/browser-page.js.map +1 -0
- package/dist/src/browser/browser-state.d.ts +7 -0
- package/dist/src/browser/browser-state.d.ts.map +1 -0
- package/dist/src/browser/browser-state.js +19 -0
- package/dist/src/browser/browser-state.js.map +1 -0
- package/dist/src/browser/browser.d.ts +5 -68
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +9 -849
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/browser/actions-eval.ts +118 -0
- package/src/browser/actions-form.ts +218 -0
- package/src/browser/actions-keyboard.ts +171 -0
- package/src/browser/actions-mouse.ts +212 -0
- package/src/browser/actions.ts +37 -712
- package/src/browser/browser-discovery.ts +116 -0
- package/src/browser/browser-launch.ts +358 -0
- package/src/browser/browser-lifecycle.ts +238 -0
- package/src/browser/browser-page.ts +213 -0
- package/src/browser/browser-state.ts +21 -0
- package/src/browser/browser.ts +17 -917
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
|
|
3
|
+
export async function pushState(client: CdpClient, sessionId: string, url: string): Promise<void> {
|
|
4
|
+
// Try Next.js router first, then fallback to history.pushState
|
|
5
|
+
await evaluateJs(
|
|
6
|
+
client,
|
|
7
|
+
sessionId,
|
|
8
|
+
`
|
|
9
|
+
(function() {
|
|
10
|
+
const url = ${JSON.stringify(url)};
|
|
11
|
+
if (window.next && window.next.router) {
|
|
12
|
+
window.next.router.push(url).catch(function() {});
|
|
13
|
+
return;
|
|
14
|
+
} else {
|
|
15
|
+
history.pushState({}, '', url);
|
|
16
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
17
|
+
}
|
|
18
|
+
})()
|
|
19
|
+
`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function addInitScript(
|
|
24
|
+
client: CdpClient,
|
|
25
|
+
sessionId: string,
|
|
26
|
+
script: string,
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
const result = await client.send<{ identifier: string }>(
|
|
29
|
+
'Page.addScriptToEvaluateOnNewDocument',
|
|
30
|
+
{
|
|
31
|
+
source: script,
|
|
32
|
+
},
|
|
33
|
+
sessionId,
|
|
34
|
+
);
|
|
35
|
+
return result.identifier;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function removeInitScript(
|
|
39
|
+
client: CdpClient,
|
|
40
|
+
sessionId: string,
|
|
41
|
+
identifier: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
await client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier }, sessionId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Default cap on how long an evaluateJs() call waits for Runtime.evaluate to
|
|
47
|
+
// resolve. Because this always passes `awaitPromise: true`, an expression
|
|
48
|
+
// like `new Promise(() => {})` never settles — the underlying CdpClient.send()
|
|
49
|
+
// has no timeout of its own (the CDP_RESPONSE_SIZE_LIMIT in cdp.ts only guards
|
|
50
|
+
// the HTTP /json/* endpoints, not WebSocket command responses), so without
|
|
51
|
+
// this the caller (e.g. the `eval` CLI command) would hang forever with
|
|
52
|
+
// nothing to kill it.
|
|
53
|
+
const DEFAULT_EVAL_TIMEOUT_MS = 30_000;
|
|
54
|
+
|
|
55
|
+
export async function evaluateJs(
|
|
56
|
+
client: CdpClient,
|
|
57
|
+
sessionId: string,
|
|
58
|
+
expression: string,
|
|
59
|
+
timeoutMs: number = DEFAULT_EVAL_TIMEOUT_MS,
|
|
60
|
+
): Promise<unknown> {
|
|
61
|
+
const evalPromise = client.send<{
|
|
62
|
+
result: { value?: unknown; type: string; description?: string };
|
|
63
|
+
exceptionDetails?: { text: string; exception?: { description?: string } };
|
|
64
|
+
}>(
|
|
65
|
+
'Runtime.evaluate',
|
|
66
|
+
{
|
|
67
|
+
expression,
|
|
68
|
+
returnByValue: true,
|
|
69
|
+
awaitPromise: true,
|
|
70
|
+
},
|
|
71
|
+
sessionId,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Not unref'd (same rule as CdpClient.send): this timer settles the awaited
|
|
75
|
+
// race, so it has to hold the event loop open while the evaluation is in
|
|
76
|
+
// flight. Cleared in the finally so a fast evaluation does not keep the
|
|
77
|
+
// process alive for the rest of timeoutMs.
|
|
78
|
+
let evalTimer: ReturnType<typeof setTimeout> | undefined;
|
|
79
|
+
let result: Awaited<typeof evalPromise>;
|
|
80
|
+
try {
|
|
81
|
+
result = await (timeoutMs > 0
|
|
82
|
+
? Promise.race([
|
|
83
|
+
evalPromise,
|
|
84
|
+
new Promise<never>((_, reject) => {
|
|
85
|
+
evalTimer = setTimeout(
|
|
86
|
+
() => reject(new Error(`JS evaluation timed out after ${timeoutMs}ms`)),
|
|
87
|
+
timeoutMs,
|
|
88
|
+
);
|
|
89
|
+
}),
|
|
90
|
+
])
|
|
91
|
+
: evalPromise);
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(evalTimer);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (result.exceptionDetails) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`JS evaluation error: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result.result?.value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function readClipboard(client: CdpClient, sessionId: string): Promise<string> {
|
|
106
|
+
const result = await evaluateJs(client, sessionId, 'navigator.clipboard.readText()');
|
|
107
|
+
return result as string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function writeClipboard(
|
|
111
|
+
client: CdpClient,
|
|
112
|
+
sessionId: string,
|
|
113
|
+
text: string,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
// Cap to 100 KB to prevent OOM when serializing the CDP expression
|
|
116
|
+
const safeText = text.length > 102_400 ? text.slice(0, 102_400) : text;
|
|
117
|
+
await evaluateJs(client, sessionId, `navigator.clipboard.writeText(${JSON.stringify(safeText)})`);
|
|
118
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pressKeyCombo, typeText } from './actions-keyboard.js';
|
|
3
|
+
import { clickPoint } from './actions-mouse.js';
|
|
4
|
+
import type { CdpClient } from './cdp.js';
|
|
5
|
+
import { getElementBox, getObjectIdForRef } from './snapshot.js';
|
|
6
|
+
import type { ClickOptions, ElementRef } from './types.js';
|
|
7
|
+
|
|
8
|
+
export async function clickElement(
|
|
9
|
+
client: CdpClient,
|
|
10
|
+
sessionId: string,
|
|
11
|
+
ref: ElementRef,
|
|
12
|
+
options: ClickOptions = {},
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const box = await getElementBox(client, sessionId, ref);
|
|
15
|
+
|
|
16
|
+
if (box) {
|
|
17
|
+
await clickPoint(client, sessionId, box.x, box.y, options);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback: use JS click via objectId
|
|
22
|
+
const objectId = await getObjectIdForRef(client, sessionId, ref);
|
|
23
|
+
if (objectId) {
|
|
24
|
+
await client.send(
|
|
25
|
+
'Runtime.callFunctionOn',
|
|
26
|
+
{
|
|
27
|
+
functionDeclaration: 'function() { this.click(); }',
|
|
28
|
+
objectId,
|
|
29
|
+
returnByValue: true,
|
|
30
|
+
},
|
|
31
|
+
sessionId,
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error(`Cannot click ref @${ref.ref}: element not found in DOM`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function fillElement(
|
|
40
|
+
client: CdpClient,
|
|
41
|
+
sessionId: string,
|
|
42
|
+
ref: ElementRef,
|
|
43
|
+
value: string,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const box = await getElementBox(client, sessionId, ref);
|
|
46
|
+
|
|
47
|
+
if (box) {
|
|
48
|
+
// Click to focus
|
|
49
|
+
await clickPoint(client, sessionId, box.x, box.y);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Select all and replace
|
|
53
|
+
const objectId = await getObjectIdForRef(client, sessionId, ref);
|
|
54
|
+
if (objectId) {
|
|
55
|
+
const fillSelectResult = await client.send<{
|
|
56
|
+
result: unknown;
|
|
57
|
+
exceptionDetails?: { text: string; exception?: { description?: string } };
|
|
58
|
+
}>(
|
|
59
|
+
'Runtime.callFunctionOn',
|
|
60
|
+
{
|
|
61
|
+
functionDeclaration: `function() {
|
|
62
|
+
this.focus();
|
|
63
|
+
if (this.tagName === 'INPUT' || this.tagName === 'TEXTAREA') {
|
|
64
|
+
this.select();
|
|
65
|
+
} else if (this.isContentEditable) {
|
|
66
|
+
const range = document.createRange();
|
|
67
|
+
range.selectNodeContents(this);
|
|
68
|
+
const sel = window.getSelection();
|
|
69
|
+
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
|
70
|
+
}
|
|
71
|
+
}`,
|
|
72
|
+
objectId,
|
|
73
|
+
returnByValue: true,
|
|
74
|
+
},
|
|
75
|
+
sessionId,
|
|
76
|
+
);
|
|
77
|
+
if (fillSelectResult.exceptionDetails) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`fillElement select-all failed: ${fillSelectResult.exceptionDetails.exception?.description ?? fillSelectResult.exceptionDetails.text}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} else if (box) {
|
|
83
|
+
// Fallback for elements without a resolvable objectId: keyboard select-all clears existing content
|
|
84
|
+
const mod = process.platform === 'darwin' ? 4 : 2;
|
|
85
|
+
await pressKeyCombo(client, sessionId, 'a', mod);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!objectId && !box) throw new Error(`Cannot fill ref @${ref.ref}: element not found in DOM`);
|
|
89
|
+
|
|
90
|
+
// Type the value character by character for natural input
|
|
91
|
+
await typeText(client, sessionId, value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function selectOption(
|
|
95
|
+
client: CdpClient,
|
|
96
|
+
sessionId: string,
|
|
97
|
+
ref: ElementRef,
|
|
98
|
+
value: string,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const objectId = await getObjectIdForRef(client, sessionId, ref);
|
|
101
|
+
if (!objectId) throw new Error(`Cannot select: ref @${ref.ref} not found in DOM`);
|
|
102
|
+
|
|
103
|
+
const selectResult = await client.send<{
|
|
104
|
+
result: unknown;
|
|
105
|
+
exceptionDetails?: { text: string; exception?: { description?: string } };
|
|
106
|
+
}>(
|
|
107
|
+
'Runtime.callFunctionOn',
|
|
108
|
+
{
|
|
109
|
+
functionDeclaration: `function(value) {
|
|
110
|
+
if (this.tagName !== 'SELECT') throw new Error('Not a select element');
|
|
111
|
+
for (const opt of this.options) {
|
|
112
|
+
if (opt.value === value || opt.textContent.trim() === value) {
|
|
113
|
+
this.value = opt.value;
|
|
114
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw new Error('Option not found: ' + value);
|
|
119
|
+
}`,
|
|
120
|
+
objectId,
|
|
121
|
+
arguments: [{ value }],
|
|
122
|
+
returnByValue: true,
|
|
123
|
+
},
|
|
124
|
+
sessionId,
|
|
125
|
+
);
|
|
126
|
+
if (selectResult.exceptionDetails) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`selectOption failed: ${selectResult.exceptionDetails.exception?.description ?? selectResult.exceptionDetails.text}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function checkElement(
|
|
134
|
+
client: CdpClient,
|
|
135
|
+
sessionId: string,
|
|
136
|
+
ref: ElementRef,
|
|
137
|
+
checked = true,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const objectId = await getObjectIdForRef(client, sessionId, ref);
|
|
140
|
+
if (!objectId) throw new Error(`Cannot check: ref @${ref.ref} not found in DOM`);
|
|
141
|
+
|
|
142
|
+
const checkResult = await client.send<{
|
|
143
|
+
result: unknown;
|
|
144
|
+
exceptionDetails?: { text: string; exception?: { description?: string } };
|
|
145
|
+
}>(
|
|
146
|
+
'Runtime.callFunctionOn',
|
|
147
|
+
{
|
|
148
|
+
functionDeclaration: `function(checked) {
|
|
149
|
+
if (this.checked !== checked) {
|
|
150
|
+
this.click();
|
|
151
|
+
}
|
|
152
|
+
}`,
|
|
153
|
+
objectId,
|
|
154
|
+
arguments: [{ value: checked }],
|
|
155
|
+
returnByValue: true,
|
|
156
|
+
},
|
|
157
|
+
sessionId,
|
|
158
|
+
);
|
|
159
|
+
if (checkResult.exceptionDetails) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`checkElement failed: ${checkResult.exceptionDetails.exception?.description ?? checkResult.exceptionDetails.text}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function focusElement(
|
|
167
|
+
client: CdpClient,
|
|
168
|
+
sessionId: string,
|
|
169
|
+
ref: ElementRef,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const objectId = await getObjectIdForRef(client, sessionId, ref);
|
|
172
|
+
if (objectId) {
|
|
173
|
+
await client.send(
|
|
174
|
+
'Runtime.callFunctionOn',
|
|
175
|
+
{
|
|
176
|
+
functionDeclaration: 'function() { this.focus(); }',
|
|
177
|
+
objectId,
|
|
178
|
+
returnByValue: true,
|
|
179
|
+
},
|
|
180
|
+
sessionId,
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const box = await getElementBox(client, sessionId, ref);
|
|
185
|
+
if (box) {
|
|
186
|
+
await clickPoint(client, sessionId, box.x, box.y);
|
|
187
|
+
} else {
|
|
188
|
+
throw new Error(`Cannot focus ref @${ref.ref}: element not found in DOM`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function typeIntoElement(
|
|
193
|
+
client: CdpClient,
|
|
194
|
+
sessionId: string,
|
|
195
|
+
ref: ElementRef,
|
|
196
|
+
text: string,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
await focusElement(client, sessionId, ref);
|
|
199
|
+
await typeText(client, sessionId, text);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function uploadFile(
|
|
203
|
+
client: CdpClient,
|
|
204
|
+
sessionId: string,
|
|
205
|
+
ref: ElementRef,
|
|
206
|
+
filePaths: string[],
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
if (!ref.backendDOMNodeId) throw new Error(`Cannot upload: ref @${ref.ref} has no DOM node`);
|
|
209
|
+
|
|
210
|
+
await client.send(
|
|
211
|
+
'DOM.setFileInputFiles',
|
|
212
|
+
{
|
|
213
|
+
files: filePaths.map((f) => resolve(f)),
|
|
214
|
+
backendNodeId: ref.backendDOMNodeId,
|
|
215
|
+
},
|
|
216
|
+
sessionId,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
|
|
3
|
+
export async function typeText(client: CdpClient, sessionId: string, text: string): Promise<void> {
|
|
4
|
+
// Cap to 100 KB to prevent OOM in CDP message serializer
|
|
5
|
+
const safeText = text.length > 102_400 ? text.slice(0, 102_400) : text;
|
|
6
|
+
await client.send('Input.insertText', { text: safeText }, sessionId);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function pressKeyCombo(
|
|
10
|
+
client: CdpClient,
|
|
11
|
+
sessionId: string,
|
|
12
|
+
key: string,
|
|
13
|
+
modifiers: number,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const { text: _text, ...keyInfo } = resolveKey(key);
|
|
16
|
+
// rawKeyDown prevents Chrome from inserting the character text; only the shortcut fires
|
|
17
|
+
await client.send(
|
|
18
|
+
'Input.dispatchKeyEvent',
|
|
19
|
+
{ type: 'rawKeyDown', ...keyInfo, modifiers },
|
|
20
|
+
sessionId,
|
|
21
|
+
);
|
|
22
|
+
await client.send('Input.dispatchKeyEvent', { type: 'keyUp', ...keyInfo, modifiers }, sessionId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function pressKey(client: CdpClient, sessionId: string, key: string): Promise<void> {
|
|
26
|
+
const { text, ...keyInfo } = resolveKey(key);
|
|
27
|
+
|
|
28
|
+
// rawKeyDown does not insert text; the explicit char event handles insertion
|
|
29
|
+
await client.send(
|
|
30
|
+
'Input.dispatchKeyEvent',
|
|
31
|
+
{
|
|
32
|
+
type: 'rawKeyDown',
|
|
33
|
+
...keyInfo,
|
|
34
|
+
},
|
|
35
|
+
sessionId,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (text) {
|
|
39
|
+
await client.send(
|
|
40
|
+
'Input.dispatchKeyEvent',
|
|
41
|
+
{
|
|
42
|
+
type: 'char',
|
|
43
|
+
text,
|
|
44
|
+
},
|
|
45
|
+
sessionId,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await client.send(
|
|
50
|
+
'Input.dispatchKeyEvent',
|
|
51
|
+
{
|
|
52
|
+
type: 'keyUp',
|
|
53
|
+
...keyInfo,
|
|
54
|
+
},
|
|
55
|
+
sessionId,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveKey(key: string): {
|
|
60
|
+
key: string;
|
|
61
|
+
code: string;
|
|
62
|
+
text?: string;
|
|
63
|
+
windowsVirtualKeyCode?: number;
|
|
64
|
+
} {
|
|
65
|
+
const keyMap: Record<
|
|
66
|
+
string,
|
|
67
|
+
{ key: string; code: string; text?: string; windowsVirtualKeyCode?: number }
|
|
68
|
+
> = {
|
|
69
|
+
Enter: { key: 'Enter', code: 'Enter', text: '\r', windowsVirtualKeyCode: 13 },
|
|
70
|
+
Tab: { key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 },
|
|
71
|
+
Escape: { key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 },
|
|
72
|
+
Backspace: { key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 },
|
|
73
|
+
Delete: { key: 'Delete', code: 'Delete', windowsVirtualKeyCode: 46 },
|
|
74
|
+
ArrowUp: { key: 'ArrowUp', code: 'ArrowUp', windowsVirtualKeyCode: 38 },
|
|
75
|
+
ArrowDown: { key: 'ArrowDown', code: 'ArrowDown', windowsVirtualKeyCode: 40 },
|
|
76
|
+
ArrowLeft: { key: 'ArrowLeft', code: 'ArrowLeft', windowsVirtualKeyCode: 37 },
|
|
77
|
+
ArrowRight: { key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 },
|
|
78
|
+
Home: { key: 'Home', code: 'Home', windowsVirtualKeyCode: 36 },
|
|
79
|
+
End: { key: 'End', code: 'End', windowsVirtualKeyCode: 35 },
|
|
80
|
+
PageUp: { key: 'PageUp', code: 'PageUp', windowsVirtualKeyCode: 33 },
|
|
81
|
+
PageDown: { key: 'PageDown', code: 'PageDown', windowsVirtualKeyCode: 34 },
|
|
82
|
+
Space: { key: ' ', code: 'Space', text: ' ', windowsVirtualKeyCode: 32 },
|
|
83
|
+
' ': { key: ' ', code: 'Space', text: ' ', windowsVirtualKeyCode: 32 },
|
|
84
|
+
Shift: { key: 'Shift', code: 'ShiftLeft', windowsVirtualKeyCode: 16 },
|
|
85
|
+
Control: { key: 'Control', code: 'ControlLeft', windowsVirtualKeyCode: 17 },
|
|
86
|
+
Alt: { key: 'Alt', code: 'AltLeft', windowsVirtualKeyCode: 18 },
|
|
87
|
+
Meta: { key: 'Meta', code: 'MetaLeft', windowsVirtualKeyCode: 91 },
|
|
88
|
+
F1: { key: 'F1', code: 'F1', windowsVirtualKeyCode: 112 },
|
|
89
|
+
F2: { key: 'F2', code: 'F2', windowsVirtualKeyCode: 113 },
|
|
90
|
+
F3: { key: 'F3', code: 'F3', windowsVirtualKeyCode: 114 },
|
|
91
|
+
F4: { key: 'F4', code: 'F4', windowsVirtualKeyCode: 115 },
|
|
92
|
+
F5: { key: 'F5', code: 'F5', windowsVirtualKeyCode: 116 },
|
|
93
|
+
F6: { key: 'F6', code: 'F6', windowsVirtualKeyCode: 117 },
|
|
94
|
+
F7: { key: 'F7', code: 'F7', windowsVirtualKeyCode: 118 },
|
|
95
|
+
F8: { key: 'F8', code: 'F8', windowsVirtualKeyCode: 119 },
|
|
96
|
+
F9: { key: 'F9', code: 'F9', windowsVirtualKeyCode: 120 },
|
|
97
|
+
F10: { key: 'F10', code: 'F10', windowsVirtualKeyCode: 121 },
|
|
98
|
+
F11: { key: 'F11', code: 'F11', windowsVirtualKeyCode: 122 },
|
|
99
|
+
F12: { key: 'F12', code: 'F12', windowsVirtualKeyCode: 123 },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (keyMap[key]) return keyMap[key];
|
|
103
|
+
|
|
104
|
+
// Single printable character — windowsVirtualKeyCode required for rawKeyDown shortcut dispatch
|
|
105
|
+
if (key.length === 1) {
|
|
106
|
+
const charCode = key.charCodeAt(0);
|
|
107
|
+
if (charCode >= 48 && charCode <= 57) {
|
|
108
|
+
return { key, code: `Digit${key}`, text: key, windowsVirtualKeyCode: charCode };
|
|
109
|
+
}
|
|
110
|
+
if ((charCode >= 65 && charCode <= 90) || (charCode >= 97 && charCode <= 122)) {
|
|
111
|
+
return {
|
|
112
|
+
key,
|
|
113
|
+
code: `Key${key.toUpperCase()}`,
|
|
114
|
+
text: key,
|
|
115
|
+
windowsVirtualKeyCode: key.toUpperCase().charCodeAt(0),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Symbol keys: map to the physical key's code and Windows virtual key code
|
|
119
|
+
const symbolMap: Record<string, { code: string; windowsVirtualKeyCode: number }> = {
|
|
120
|
+
'!': { code: 'Digit1', windowsVirtualKeyCode: 49 },
|
|
121
|
+
'@': { code: 'Digit2', windowsVirtualKeyCode: 50 },
|
|
122
|
+
'#': { code: 'Digit3', windowsVirtualKeyCode: 51 },
|
|
123
|
+
$: { code: 'Digit4', windowsVirtualKeyCode: 52 },
|
|
124
|
+
'%': { code: 'Digit5', windowsVirtualKeyCode: 53 },
|
|
125
|
+
'^': { code: 'Digit6', windowsVirtualKeyCode: 54 },
|
|
126
|
+
'&': { code: 'Digit7', windowsVirtualKeyCode: 55 },
|
|
127
|
+
'*': { code: 'Digit8', windowsVirtualKeyCode: 56 },
|
|
128
|
+
'(': { code: 'Digit9', windowsVirtualKeyCode: 57 },
|
|
129
|
+
')': { code: 'Digit0', windowsVirtualKeyCode: 48 },
|
|
130
|
+
'-': { code: 'Minus', windowsVirtualKeyCode: 189 },
|
|
131
|
+
_: { code: 'Minus', windowsVirtualKeyCode: 189 },
|
|
132
|
+
'=': { code: 'Equal', windowsVirtualKeyCode: 187 },
|
|
133
|
+
'+': { code: 'Equal', windowsVirtualKeyCode: 187 },
|
|
134
|
+
'[': { code: 'BracketLeft', windowsVirtualKeyCode: 219 },
|
|
135
|
+
'{': { code: 'BracketLeft', windowsVirtualKeyCode: 219 },
|
|
136
|
+
']': { code: 'BracketRight', windowsVirtualKeyCode: 221 },
|
|
137
|
+
'}': { code: 'BracketRight', windowsVirtualKeyCode: 221 },
|
|
138
|
+
'\\': { code: 'Backslash', windowsVirtualKeyCode: 220 },
|
|
139
|
+
'|': { code: 'Backslash', windowsVirtualKeyCode: 220 },
|
|
140
|
+
';': { code: 'Semicolon', windowsVirtualKeyCode: 186 },
|
|
141
|
+
':': { code: 'Semicolon', windowsVirtualKeyCode: 186 },
|
|
142
|
+
"'": { code: 'Quote', windowsVirtualKeyCode: 222 },
|
|
143
|
+
'"': { code: 'Quote', windowsVirtualKeyCode: 222 },
|
|
144
|
+
'`': { code: 'Backquote', windowsVirtualKeyCode: 192 },
|
|
145
|
+
'~': { code: 'Backquote', windowsVirtualKeyCode: 192 },
|
|
146
|
+
',': { code: 'Comma', windowsVirtualKeyCode: 188 },
|
|
147
|
+
'<': { code: 'Comma', windowsVirtualKeyCode: 188 },
|
|
148
|
+
'.': { code: 'Period', windowsVirtualKeyCode: 190 },
|
|
149
|
+
'>': { code: 'Period', windowsVirtualKeyCode: 190 },
|
|
150
|
+
'/': { code: 'Slash', windowsVirtualKeyCode: 191 },
|
|
151
|
+
'?': { code: 'Slash', windowsVirtualKeyCode: 191 },
|
|
152
|
+
};
|
|
153
|
+
const sym = symbolMap[key];
|
|
154
|
+
if (sym)
|
|
155
|
+
return { key, code: sym.code, text: key, windowsVirtualKeyCode: sym.windowsVirtualKeyCode };
|
|
156
|
+
// Unknown/non-ASCII character — omit code since no valid DOM KeyboardEvent.code exists
|
|
157
|
+
return { key, code: '', text: key };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { key, code: key };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function keyDown(client: CdpClient, sessionId: string, key: string): Promise<void> {
|
|
164
|
+
const { text: _text, ...keyInfo } = resolveKey(key);
|
|
165
|
+
await client.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', ...keyInfo }, sessionId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function keyUp(client: CdpClient, sessionId: string, key: string): Promise<void> {
|
|
169
|
+
const { text: _text, ...keyInfo } = resolveKey(key);
|
|
170
|
+
await client.send('Input.dispatchKeyEvent', { type: 'keyUp', ...keyInfo }, sessionId);
|
|
171
|
+
}
|