@dyyz1993/agent-browser 0.25.0 → 0.26.1
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/__tests__/e2e/utils/test-helpers.d.ts +2 -2
- package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
- package/dist/__tests__/e2e/utils/test-helpers.js +6 -4
- package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
- package/dist/actions/advanced.d.ts +73 -0
- package/dist/actions/advanced.d.ts.map +1 -0
- package/dist/actions/advanced.js +390 -0
- package/dist/actions/advanced.js.map +1 -0
- package/dist/actions/context.d.ts +36 -0
- package/dist/actions/context.d.ts.map +1 -0
- package/dist/actions/context.js +164 -0
- package/dist/actions/context.js.map +1 -0
- package/dist/actions/crawl.d.ts +8 -0
- package/dist/actions/crawl.d.ts.map +1 -0
- package/dist/actions/crawl.js +294 -0
- package/dist/actions/crawl.js.map +1 -0
- package/dist/actions/elements.d.ts +11 -0
- package/dist/actions/elements.d.ts.map +1 -0
- package/dist/actions/elements.js +78 -0
- package/dist/actions/elements.js.map +1 -0
- package/dist/actions/flow.d.ts +4 -0
- package/dist/actions/flow.d.ts.map +1 -0
- package/dist/actions/flow.js +170 -0
- package/dist/actions/flow.js.map +1 -0
- package/dist/actions/index.d.ts +7 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +323 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/interact.d.ts +4 -0
- package/dist/actions/interact.d.ts.map +1 -0
- package/dist/actions/interact.js +162 -0
- package/dist/actions/interact.js.map +1 -0
- package/dist/actions/interaction.d.ts +31 -0
- package/dist/actions/interaction.d.ts.map +1 -0
- package/dist/actions/interaction.js +477 -0
- package/dist/actions/interaction.js.map +1 -0
- package/dist/actions/locators.d.ts +14 -0
- package/dist/actions/locators.d.ts.map +1 -0
- package/dist/actions/locators.js +310 -0
- package/dist/actions/locators.js.map +1 -0
- package/dist/actions/map.d.ts +4 -0
- package/dist/actions/map.d.ts.map +1 -0
- package/dist/actions/map.js +79 -0
- package/dist/actions/map.js.map +1 -0
- package/dist/actions/meta.d.ts +44 -0
- package/dist/actions/meta.d.ts.map +1 -0
- package/dist/actions/meta.js +190 -0
- package/dist/actions/meta.js.map +1 -0
- package/dist/actions/mouse.d.ts +8 -0
- package/dist/actions/mouse.d.ts.map +1 -0
- package/dist/actions/mouse.js +52 -0
- package/dist/actions/mouse.js.map +1 -0
- package/dist/actions/recorder.d.ts +20 -0
- package/dist/actions/recorder.d.ts.map +1 -0
- package/dist/actions/recorder.js +231 -0
- package/dist/actions/recorder.js.map +1 -0
- package/dist/actions/recording.d.ts +6 -0
- package/dist/actions/recording.d.ts.map +1 -0
- package/dist/actions/recording.js +22 -0
- package/dist/actions/recording.js.map +1 -0
- package/dist/actions/scrape.d.ts +10 -0
- package/dist/actions/scrape.d.ts.map +1 -0
- package/dist/actions/scrape.js +40 -0
- package/dist/actions/scrape.js.map +1 -0
- package/dist/actions/screencast.d.ts +8 -0
- package/dist/actions/screencast.d.ts.map +1 -0
- package/dist/actions/screencast.js +56 -0
- package/dist/actions/screencast.js.map +1 -0
- package/dist/actions/search.d.ts +4 -0
- package/dist/actions/search.d.ts.map +1 -0
- package/dist/actions/search.js +129 -0
- package/dist/actions/search.js.map +1 -0
- package/dist/actions/storage.d.ts +14 -0
- package/dist/actions/storage.d.ts.map +1 -0
- package/dist/actions/storage.js +63 -0
- package/dist/actions/storage.js.map +1 -0
- package/dist/actions/tabs.d.ts +16 -0
- package/dist/actions/tabs.d.ts.map +1 -0
- package/dist/actions/tabs.js +47 -0
- package/dist/actions/tabs.js.map +1 -0
- package/dist/actions/utils.d.ts +16 -0
- package/dist/actions/utils.d.ts.map +1 -0
- package/dist/actions/utils.js +451 -0
- package/dist/actions/utils.js.map +1 -0
- package/dist/browser/browser-manager.d.ts +249 -0
- package/dist/browser/browser-manager.d.ts.map +1 -0
- package/dist/browser/browser-manager.js +1251 -0
- package/dist/browser/browser-manager.js.map +1 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/network-tracker.d.ts +39 -0
- package/dist/browser/network-tracker.d.ts.map +1 -0
- package/dist/browser/network-tracker.js +287 -0
- package/dist/browser/network-tracker.js.map +1 -0
- package/dist/browser/providers.d.ts +27 -0
- package/dist/browser/providers.d.ts.map +1 -0
- package/dist/browser/providers.js +293 -0
- package/dist/browser/providers.js.map +1 -0
- package/dist/browser/recorder-manager.d.ts +69 -0
- package/dist/browser/recorder-manager.d.ts.map +1 -0
- package/dist/browser/recorder-manager.js +755 -0
- package/dist/browser/recorder-manager.js.map +1 -0
- package/dist/browser/recording-manager.d.ts +46 -0
- package/dist/browser/recording-manager.d.ts.map +1 -0
- package/dist/browser/recording-manager.js +156 -0
- package/dist/browser/recording-manager.js.map +1 -0
- package/dist/browser/screencast-manager.d.ts +49 -0
- package/dist/browser/screencast-manager.d.ts.map +1 -0
- package/dist/browser/screencast-manager.js +131 -0
- package/dist/browser/screencast-manager.js.map +1 -0
- package/dist/browser/types.d.ts +101 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/browser-events.d.ts +25 -0
- package/dist/browser-events.d.ts.map +1 -0
- package/dist/browser-events.js +15 -0
- package/dist/browser-events.js.map +1 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +140 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/connection.d.ts.map +1 -1
- package/dist/cli/connection.js +15 -22
- package/dist/cli/connection.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +153 -3
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +72 -0
- package/dist/cli/output.js.map +1 -1
- package/dist/cli.js +3 -4
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +12 -13
- package/dist/daemon.js.map +1 -1
- package/dist/flow/exporters/playwright.d.ts +23 -1
- package/dist/flow/exporters/playwright.d.ts.map +1 -1
- package/dist/flow/exporters/playwright.js +333 -85
- package/dist/flow/exporters/playwright.js.map +1 -1
- package/dist/flow/exporters/python.d.ts +22 -0
- package/dist/flow/exporters/python.d.ts.map +1 -1
- package/dist/flow/exporters/python.js +325 -74
- package/dist/flow/exporters/python.js.map +1 -1
- package/dist/flow/exporters/selenium.d.ts.map +1 -1
- package/dist/flow/exporters/selenium.js +0 -1
- package/dist/flow/exporters/selenium.js.map +1 -1
- package/dist/flow/flow-executor.d.ts +1 -1
- package/dist/flow/flow-executor.d.ts.map +1 -1
- package/dist/flow/flow-executor.js +11 -11
- package/dist/flow/flow-executor.js.map +1 -1
- package/dist/flow/output.js.map +1 -1
- package/dist/flow/plugin-system.d.ts +1 -1
- package/dist/flow/plugin-system.d.ts.map +1 -1
- package/dist/flow/plugin-system.js +2 -2
- package/dist/flow/plugin-system.js.map +1 -1
- package/dist/flow/plugins/logging-plugin.js +1 -1
- package/dist/flow/plugins/logging-plugin.js.map +1 -1
- package/dist/flow/presets/console-capture.js +33 -14
- package/dist/flow/presets/fetch-capture.js +52 -23
- package/dist/flow/presets/sse-stream.js +35 -17
- package/dist/flow/presets/xhr-only.js +22 -12
- package/dist/flow/recorder-to-flow.d.ts.map +1 -1
- package/dist/flow/recorder-to-flow.js +1 -3
- package/dist/flow/recorder-to-flow.js.map +1 -1
- package/dist/flow/site-manager.d.ts.map +1 -1
- package/dist/flow/site-manager.js +6 -2
- package/dist/flow/site-manager.js.map +1 -1
- package/dist/human-mouse.d.ts +1 -1
- package/dist/human-mouse.d.ts.map +1 -1
- package/dist/human-mouse.js +2 -2
- package/dist/human-mouse.js.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +90 -0
- package/dist/protocol.js.map +1 -1
- package/dist/rc-config.js +4 -4
- package/dist/rc-config.js.map +1 -1
- package/dist/recorder/inject.js +31 -5
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +3 -4
- package/dist/snapshot.js.map +1 -1
- package/dist/stream-server-standalone.d.ts +1 -1
- package/dist/stream-server-standalone.d.ts.map +1 -1
- package/dist/stream-server-standalone.js +42 -23
- package/dist/stream-server-standalone.js.map +1 -1
- package/dist/stream-server.d.ts +1 -1
- package/dist/stream-server.d.ts.map +1 -1
- package/dist/stream-server.js +26 -21
- package/dist/stream-server.js.map +1 -1
- package/dist/test-live.js +9 -3
- package/dist/test-live.js.map +1 -1
- package/dist/types.d.ts +122 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -3
- package/scripts/README.md +66 -0
- package/scripts/copy-flow-presets.js +25 -0
- package/scripts/douyin-flow-test.sh +72 -0
- package/scripts/douyin-test.sh +101 -0
- package/dist/actions.d.ts +0 -51
- package/dist/actions.d.ts.map +0 -1
- package/dist/actions.js +0 -2662
- package/dist/actions.js.map +0 -1
- package/dist/browser.d.ts +0 -651
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js +0 -3088
- package/dist/browser.js.map +0 -1
- package/dist/ios-actions.d.ts +0 -11
- package/dist/ios-actions.d.ts.map +0 -1
- package/dist/ios-actions.js +0 -228
- package/dist/ios-actions.js.map +0 -1
- package/dist/ios-manager.d.ts +0 -266
- package/dist/ios-manager.d.ts.map +0 -1
- package/dist/ios-manager.js +0 -1076
- package/dist/ios-manager.js.map +0 -1
package/dist/actions.js
DELETED
|
@@ -1,2662 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, existsSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { getAppDir, getSession, getInstanceId } from './daemon.js';
|
|
4
|
-
import { performDiff } from './diff.js';
|
|
5
|
-
import { MessageBridge } from './message-bridge.js';
|
|
6
|
-
import { getViewerUrl, getViewerWsUrl, getViewerPort, getMessageBridgeUrl, getExecutablePath, getEffectiveValue, loadConfig, } from './rc-config.js';
|
|
7
|
-
import { detectMainContent, generateContentTips } from './content-detection.js';
|
|
8
|
-
import { humanClick, humanType, humanMoveTo, humanWander, getHumanConfigFromEnv, } from './human-mouse.js';
|
|
9
|
-
import { successResponse, errorResponse } from './protocol.js';
|
|
10
|
-
import { parseYamlSiteFile, loadSitesFromDirectory, loadAllSites, findFlow, validateYamlFile, } from './flow/yaml-parser.js';
|
|
11
|
-
import { recorderToFlowFromFile, siteToYamlString } from './flow/recorder-to-flow.js';
|
|
12
|
-
import { FlowExecutor } from './flow/flow-executor.js';
|
|
13
|
-
import { PlaywrightExporter, PythonExporter, CypressExporter, SeleniumExporter, } from './flow/exporters/index.js';
|
|
14
|
-
// Callback for screencast frames - will be set by the daemon when streaming is active
|
|
15
|
-
let screencastFrameCallback = null;
|
|
16
|
-
/**
|
|
17
|
-
* Set the callback for screencast frames
|
|
18
|
-
* This is called by the daemon to set up frame streaming
|
|
19
|
-
*/
|
|
20
|
-
export function setScreencastFrameCallback(callback) {
|
|
21
|
-
screencastFrameCallback = callback;
|
|
22
|
-
}
|
|
23
|
-
// Event callbacks registered by StreamServer or other listeners
|
|
24
|
-
let eventCallbacks = {};
|
|
25
|
-
/**
|
|
26
|
-
* Set browser event callbacks for real-time broadcasting
|
|
27
|
-
* This is called by StreamServer to register event listeners
|
|
28
|
-
*/
|
|
29
|
-
export function setEventCallbacks(callbacks) {
|
|
30
|
-
eventCallbacks = { ...eventCallbacks, ...callbacks };
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get the current event callbacks
|
|
34
|
-
* This is called by BrowserManager to trigger events
|
|
35
|
-
*/
|
|
36
|
-
export function getEventCallbacks() {
|
|
37
|
-
return eventCallbacks;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Quick check if element exists before performing action.
|
|
41
|
-
* This avoids waiting for timeout when element is clearly not present.
|
|
42
|
-
*/
|
|
43
|
-
async function assertElementExists(locator, selector, isRef) {
|
|
44
|
-
const count = await locator.count();
|
|
45
|
-
if (count === 0) {
|
|
46
|
-
if (isRef) {
|
|
47
|
-
throw new Error(`Element ref "${selector}" not found. ` + `Run 'snapshot' to get updated element refs.`);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
throw new Error(`No element matches selector "${selector}". ` + `Run 'snapshot' to see available elements.`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Convert Playwright errors to AI-friendly messages
|
|
56
|
-
* @internal Exported for testing
|
|
57
|
-
*/
|
|
58
|
-
export function toAIFriendlyError(error, selector) {
|
|
59
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
-
if (message.includes('strict mode violation')) {
|
|
61
|
-
const countMatch = message.match(/resolved to (\d+) elements/);
|
|
62
|
-
const count = countMatch ? countMatch[1] : 'multiple';
|
|
63
|
-
return new Error(`Selector "${selector}" matched ${count} elements. ` +
|
|
64
|
-
`Run 'snapshot' to get updated refs, or use a more specific CSS selector. ` +
|
|
65
|
-
`Tip: Use 'find nth <index> ${selector} --click' to target a specific match.`);
|
|
66
|
-
}
|
|
67
|
-
if (message.includes('intercepts pointer events')) {
|
|
68
|
-
return new Error(`Element "${selector}" is blocked by another element (likely a modal or overlay). ` +
|
|
69
|
-
`Try dismissing any modals/cookie banners first. ` +
|
|
70
|
-
`Tip: Run 'snapshot -i' to see all visible elements and identify what's blocking.`);
|
|
71
|
-
}
|
|
72
|
-
if (message.includes('not visible') && !message.includes('Timeout')) {
|
|
73
|
-
return new Error(`Element "${selector}" is not visible. ` +
|
|
74
|
-
`Try 'scrollintoview ${selector}' or check if it's hidden. ` +
|
|
75
|
-
`Tip: Run 'is visible ${selector}' to confirm visibility state.`);
|
|
76
|
-
}
|
|
77
|
-
if (message.includes('Timeout') && message.includes('exceeded')) {
|
|
78
|
-
return new Error(`Action on "${selector}" timed out. The element may be blocked, still loading, or not interactable. ` +
|
|
79
|
-
`Run 'snapshot' to check the current page state. ` +
|
|
80
|
-
`Tip: If the page is still loading, try 'wait --load networkidle' first.`);
|
|
81
|
-
}
|
|
82
|
-
if (message.includes('waiting for') &&
|
|
83
|
-
(message.includes('to be visible') || message.includes('Timeout'))) {
|
|
84
|
-
return new Error(`Element "${selector}" not found or not visible. ` +
|
|
85
|
-
`Run 'snapshot -i' to see current page elements and their refs. ` +
|
|
86
|
-
`Tip: If using @ref, the page may have changed. Re-run 'snapshot -i' to get fresh refs.`);
|
|
87
|
-
}
|
|
88
|
-
if (message.includes('Execution context was destroyed') || message.includes('Target closed')) {
|
|
89
|
-
return new Error(`Browser context was lost (page navigated or closed). ` +
|
|
90
|
-
`Re-open the page with 'open <url>' and start fresh. ` +
|
|
91
|
-
`Tip: This usually happens after a form submission triggers navigation.`);
|
|
92
|
-
}
|
|
93
|
-
if (message.includes('querySelector') || message.includes('is not a valid selector')) {
|
|
94
|
-
return new Error(`Invalid selector "${selector}". ` +
|
|
95
|
-
`CSS selectors like '#id', '.class', or 'tag' are supported. ` +
|
|
96
|
-
`Tip: Use 'snapshot -i' to get @ref selectors (e.g., @e1) that are always valid.`);
|
|
97
|
-
}
|
|
98
|
-
return error instanceof Error ? error : new Error(message);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Execute a command and return a response
|
|
102
|
-
*/
|
|
103
|
-
export async function executeCommand(command, browser) {
|
|
104
|
-
try {
|
|
105
|
-
if (command.action === 'flow') {
|
|
106
|
-
return await handleFlowAction(command, browser);
|
|
107
|
-
}
|
|
108
|
-
const cmd = command;
|
|
109
|
-
switch (cmd.action) {
|
|
110
|
-
case 'launch':
|
|
111
|
-
return await handleLaunch(cmd, browser);
|
|
112
|
-
case 'navigate':
|
|
113
|
-
return await handleNavigate(cmd, browser);
|
|
114
|
-
case 'click':
|
|
115
|
-
return await handleClick(cmd, browser);
|
|
116
|
-
case 'type':
|
|
117
|
-
return await handleType(cmd, browser);
|
|
118
|
-
case 'fill':
|
|
119
|
-
return await handleFill(cmd, browser);
|
|
120
|
-
case 'check':
|
|
121
|
-
return await handleCheck(cmd, browser);
|
|
122
|
-
case 'uncheck':
|
|
123
|
-
return await handleUncheck(cmd, browser);
|
|
124
|
-
case 'upload':
|
|
125
|
-
return await handleUpload(cmd, browser);
|
|
126
|
-
case 'dblclick':
|
|
127
|
-
return await handleDoubleClick(cmd, browser);
|
|
128
|
-
case 'focus':
|
|
129
|
-
return await handleFocus(cmd, browser);
|
|
130
|
-
case 'drag':
|
|
131
|
-
return await handleDrag(cmd, browser);
|
|
132
|
-
case 'getbyrole':
|
|
133
|
-
return await handleGetByRole(cmd, browser);
|
|
134
|
-
case 'getbytext':
|
|
135
|
-
return await handleGetByText(cmd, browser);
|
|
136
|
-
case 'getbylabel':
|
|
137
|
-
return await handleGetByLabel(cmd, browser);
|
|
138
|
-
case 'getbyplaceholder':
|
|
139
|
-
return await handleGetByPlaceholder(cmd, browser);
|
|
140
|
-
case 'press':
|
|
141
|
-
return await handlePress(cmd, browser);
|
|
142
|
-
case 'screenshot':
|
|
143
|
-
return await handleScreenshot(cmd, browser);
|
|
144
|
-
case 'snapshot':
|
|
145
|
-
return await handleSnapshot(cmd, browser);
|
|
146
|
-
case 'evaluate':
|
|
147
|
-
return await handleEvaluate(cmd, browser);
|
|
148
|
-
case 'wait':
|
|
149
|
-
return await handleWait(cmd, browser);
|
|
150
|
-
case 'scroll':
|
|
151
|
-
return await handleScroll(cmd, browser);
|
|
152
|
-
case 'select':
|
|
153
|
-
return await handleSelect(cmd, browser);
|
|
154
|
-
case 'hover':
|
|
155
|
-
return await handleHover(cmd, browser);
|
|
156
|
-
case 'content':
|
|
157
|
-
return await handleContent(cmd, browser);
|
|
158
|
-
case 'close':
|
|
159
|
-
return await handleClose(cmd, browser);
|
|
160
|
-
case 'tab_new':
|
|
161
|
-
return await handleTabNew(cmd, browser);
|
|
162
|
-
case 'tab_list':
|
|
163
|
-
return await handleTabList(cmd, browser);
|
|
164
|
-
case 'frames':
|
|
165
|
-
return await handleFrames(cmd, browser);
|
|
166
|
-
case 'tab_switch':
|
|
167
|
-
return await handleTabSwitch(cmd, browser);
|
|
168
|
-
case 'tab_close':
|
|
169
|
-
return await handleTabClose(cmd, browser);
|
|
170
|
-
case 'window_new':
|
|
171
|
-
return await handleWindowNew(cmd, browser);
|
|
172
|
-
case 'cookies_get':
|
|
173
|
-
return await handleCookiesGet(cmd, browser);
|
|
174
|
-
case 'cookies_set':
|
|
175
|
-
return await handleCookiesSet(cmd, browser);
|
|
176
|
-
case 'cookies_clear':
|
|
177
|
-
return await handleCookiesClear(cmd, browser);
|
|
178
|
-
case 'storage_get':
|
|
179
|
-
return await handleStorageGet(cmd, browser);
|
|
180
|
-
case 'storage_set':
|
|
181
|
-
return await handleStorageSet(cmd, browser);
|
|
182
|
-
case 'storage_clear':
|
|
183
|
-
return await handleStorageClear(cmd, browser);
|
|
184
|
-
case 'dialog':
|
|
185
|
-
return await handleDialog(cmd, browser);
|
|
186
|
-
case 'pdf':
|
|
187
|
-
return await handlePdf(cmd, browser);
|
|
188
|
-
case 'route':
|
|
189
|
-
return await handleRoute(cmd, browser);
|
|
190
|
-
case 'unroute':
|
|
191
|
-
return await handleUnroute(cmd, browser);
|
|
192
|
-
case 'requests':
|
|
193
|
-
return await handleRequests(cmd, browser);
|
|
194
|
-
case 'websockets':
|
|
195
|
-
return await handleWebSockets(cmd, browser);
|
|
196
|
-
case 'download':
|
|
197
|
-
return await handleDownload(cmd, browser);
|
|
198
|
-
case 'geolocation':
|
|
199
|
-
return await handleGeolocation(cmd, browser);
|
|
200
|
-
case 'permissions':
|
|
201
|
-
return await handlePermissions(cmd, browser);
|
|
202
|
-
case 'viewport':
|
|
203
|
-
return await handleViewport(cmd, browser);
|
|
204
|
-
case 'useragent':
|
|
205
|
-
return await handleUserAgent(cmd, browser);
|
|
206
|
-
case 'device':
|
|
207
|
-
return await handleDevice(cmd, browser);
|
|
208
|
-
case 'back':
|
|
209
|
-
return await handleBack(cmd, browser);
|
|
210
|
-
case 'forward':
|
|
211
|
-
return await handleForward(cmd, browser);
|
|
212
|
-
case 'reload':
|
|
213
|
-
return await handleReload(cmd, browser);
|
|
214
|
-
case 'url':
|
|
215
|
-
return await handleUrl(cmd, browser);
|
|
216
|
-
case 'title':
|
|
217
|
-
return await handleTitle(cmd, browser);
|
|
218
|
-
case 'getattribute':
|
|
219
|
-
return await handleGetAttribute(cmd, browser);
|
|
220
|
-
case 'gettext':
|
|
221
|
-
return await handleGetText(cmd, browser);
|
|
222
|
-
case 'isvisible':
|
|
223
|
-
return await handleIsVisible(cmd, browser);
|
|
224
|
-
case 'isenabled':
|
|
225
|
-
return await handleIsEnabled(cmd, browser);
|
|
226
|
-
case 'ischecked':
|
|
227
|
-
return await handleIsChecked(cmd, browser);
|
|
228
|
-
case 'count':
|
|
229
|
-
return await handleCount(cmd, browser);
|
|
230
|
-
case 'boundingbox':
|
|
231
|
-
return await handleBoundingBox(cmd, browser);
|
|
232
|
-
case 'styles':
|
|
233
|
-
return await handleStyles(cmd, browser);
|
|
234
|
-
case 'video_start':
|
|
235
|
-
return await handleVideoStart(cmd, browser);
|
|
236
|
-
case 'video_stop':
|
|
237
|
-
return await handleVideoStop(cmd, browser);
|
|
238
|
-
case 'trace_start':
|
|
239
|
-
return await handleTraceStart(cmd, browser);
|
|
240
|
-
case 'trace_stop':
|
|
241
|
-
return await handleTraceStop(cmd, browser);
|
|
242
|
-
case 'har_start':
|
|
243
|
-
return await handleHarStart(cmd, browser);
|
|
244
|
-
case 'har_stop':
|
|
245
|
-
return await handleHarStop(cmd, browser);
|
|
246
|
-
case 'state_save':
|
|
247
|
-
return await handleStateSave(cmd, browser);
|
|
248
|
-
case 'state_load':
|
|
249
|
-
return await handleStateLoad(cmd, browser);
|
|
250
|
-
case 'console':
|
|
251
|
-
return await handleConsole(cmd, browser);
|
|
252
|
-
case 'errors':
|
|
253
|
-
return await handleErrors(cmd, browser);
|
|
254
|
-
case 'keyboard':
|
|
255
|
-
return await handleKeyboard(cmd, browser);
|
|
256
|
-
case 'wheel':
|
|
257
|
-
return await handleWheel(cmd, browser);
|
|
258
|
-
case 'tap':
|
|
259
|
-
return await handleTap(cmd, browser);
|
|
260
|
-
case 'clipboard':
|
|
261
|
-
return await handleClipboard(cmd, browser);
|
|
262
|
-
case 'highlight':
|
|
263
|
-
return await handleHighlight(cmd, browser);
|
|
264
|
-
case 'clear':
|
|
265
|
-
return await handleClear(cmd, browser);
|
|
266
|
-
case 'selectall':
|
|
267
|
-
return await handleSelectAll(cmd, browser);
|
|
268
|
-
case 'innertext':
|
|
269
|
-
return await handleInnerText(cmd, browser);
|
|
270
|
-
case 'innerhtml':
|
|
271
|
-
return await handleInnerHtml(cmd, browser);
|
|
272
|
-
case 'inputvalue':
|
|
273
|
-
return await handleInputValue(cmd, browser);
|
|
274
|
-
case 'setvalue':
|
|
275
|
-
return await handleSetValue(cmd, browser);
|
|
276
|
-
case 'dispatch':
|
|
277
|
-
return await handleDispatch(cmd, browser);
|
|
278
|
-
case 'evalhandle':
|
|
279
|
-
return await handleEvalHandle(cmd, browser);
|
|
280
|
-
case 'expose':
|
|
281
|
-
return await handleExpose(cmd, browser);
|
|
282
|
-
case 'addscript':
|
|
283
|
-
return await handleAddScript(cmd, browser);
|
|
284
|
-
case 'addstyle':
|
|
285
|
-
return await handleAddStyle(cmd, browser);
|
|
286
|
-
case 'emulatemedia':
|
|
287
|
-
return await handleEmulateMedia(cmd, browser);
|
|
288
|
-
case 'offline':
|
|
289
|
-
return await handleOffline(cmd, browser);
|
|
290
|
-
case 'headers':
|
|
291
|
-
return await handleHeaders(cmd, browser);
|
|
292
|
-
case 'pause':
|
|
293
|
-
return await handlePause(cmd, browser);
|
|
294
|
-
case 'getbyalttext':
|
|
295
|
-
return await handleGetByAltText(cmd, browser);
|
|
296
|
-
case 'getbytitle':
|
|
297
|
-
return await handleGetByTitle(cmd, browser);
|
|
298
|
-
case 'getbytestid':
|
|
299
|
-
return await handleGetByTestId(cmd, browser);
|
|
300
|
-
case 'nth':
|
|
301
|
-
return await handleNth(cmd, browser);
|
|
302
|
-
case 'waitforurl':
|
|
303
|
-
return await handleWaitForUrl(cmd, browser);
|
|
304
|
-
case 'waitforloadstate':
|
|
305
|
-
return await handleWaitForLoadState(cmd, browser);
|
|
306
|
-
case 'setcontent':
|
|
307
|
-
return await handleSetContent(cmd, browser);
|
|
308
|
-
case 'timezone':
|
|
309
|
-
return await handleTimezone(cmd, browser);
|
|
310
|
-
case 'locale':
|
|
311
|
-
return await handleLocale(cmd, browser);
|
|
312
|
-
case 'credentials':
|
|
313
|
-
return await handleCredentials(cmd, browser);
|
|
314
|
-
case 'mousemove':
|
|
315
|
-
return await handleMouseMove(cmd, browser);
|
|
316
|
-
case 'mousedown':
|
|
317
|
-
return await handleMouseDown(cmd, browser);
|
|
318
|
-
case 'mouseup':
|
|
319
|
-
return await handleMouseUp(cmd, browser);
|
|
320
|
-
case 'wander':
|
|
321
|
-
return await handleWander(cmd, browser);
|
|
322
|
-
case 'mousetrajectory':
|
|
323
|
-
return await handleMouseTrajectory(cmd, browser);
|
|
324
|
-
case 'bringtofront':
|
|
325
|
-
return await handleBringToFront(cmd, browser);
|
|
326
|
-
case 'waitforfunction':
|
|
327
|
-
return await handleWaitForFunction(cmd, browser);
|
|
328
|
-
case 'scrollintoview':
|
|
329
|
-
return await handleScrollIntoView(cmd, browser);
|
|
330
|
-
case 'addinitscript':
|
|
331
|
-
return await handleAddInitScript(cmd, browser);
|
|
332
|
-
case 'keydown':
|
|
333
|
-
return await handleKeyDown(cmd, browser);
|
|
334
|
-
case 'keyup':
|
|
335
|
-
return await handleKeyUp(cmd, browser);
|
|
336
|
-
case 'inserttext':
|
|
337
|
-
return await handleInsertText(cmd, browser);
|
|
338
|
-
case 'multiselect':
|
|
339
|
-
return await handleMultiSelect(cmd, browser);
|
|
340
|
-
case 'waitfordownload':
|
|
341
|
-
return await handleWaitForDownload(cmd, browser);
|
|
342
|
-
case 'responsebody':
|
|
343
|
-
return await handleResponseBody(cmd, browser);
|
|
344
|
-
case 'screencast_start':
|
|
345
|
-
return await handleScreencastStart(cmd, browser);
|
|
346
|
-
case 'screencast_stop':
|
|
347
|
-
return await handleScreencastStop(cmd, browser);
|
|
348
|
-
case 'input_mouse':
|
|
349
|
-
return await handleInputMouse(cmd, browser);
|
|
350
|
-
case 'input_keyboard':
|
|
351
|
-
return await handleInputKeyboard(cmd, browser);
|
|
352
|
-
case 'input_touch':
|
|
353
|
-
return await handleInputTouch(cmd, browser);
|
|
354
|
-
case 'recording_start':
|
|
355
|
-
return await handleRecordingStart(cmd, browser);
|
|
356
|
-
case 'recording_stop':
|
|
357
|
-
return await handleRecordingStop(cmd, browser);
|
|
358
|
-
case 'recording_restart':
|
|
359
|
-
return await handleRecordingRestart(cmd, browser);
|
|
360
|
-
case 'recorder_start':
|
|
361
|
-
return await handleRecorderStart(cmd, browser);
|
|
362
|
-
case 'recorder_stop':
|
|
363
|
-
return await handleRecorderStop(cmd, browser);
|
|
364
|
-
case 'recorder_status':
|
|
365
|
-
return await handleRecorderStatus(cmd, browser);
|
|
366
|
-
case 'recorder_replay':
|
|
367
|
-
return await handleRecorderReplay(cmd, browser);
|
|
368
|
-
case 'viewer':
|
|
369
|
-
return await handleViewer(cmd, browser);
|
|
370
|
-
case 'ask':
|
|
371
|
-
return await handleAsk(cmd, browser);
|
|
372
|
-
case 'config':
|
|
373
|
-
return handleConfig(cmd);
|
|
374
|
-
case 'history':
|
|
375
|
-
return await handleHistory(cmd, browser);
|
|
376
|
-
case 'selector-for':
|
|
377
|
-
return await handleSelectorFor(cmd, browser);
|
|
378
|
-
case 'selectors-of':
|
|
379
|
-
return await handleSelectorsOf(cmd, browser);
|
|
380
|
-
case 'validate':
|
|
381
|
-
return await handleValidate(cmd, browser);
|
|
382
|
-
default: {
|
|
383
|
-
const unknownCommand = cmd;
|
|
384
|
-
return errorResponse(unknownCommand.id, `Unknown action: ${unknownCommand.action}`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
-
return errorResponse(command.id, message);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
async function handleLaunch(command, browser) {
|
|
394
|
-
await browser.launch(command);
|
|
395
|
-
const instanceId = getInstanceId();
|
|
396
|
-
return successResponse(command.id, {
|
|
397
|
-
launched: true,
|
|
398
|
-
instanceId,
|
|
399
|
-
viewerUrl: getViewerUrl(instanceId),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
async function handleNavigate(command, browser) {
|
|
403
|
-
const page = browser.getPage();
|
|
404
|
-
if (command.headers && Object.keys(command.headers).length > 0) {
|
|
405
|
-
await browser.setScopedHeaders(command.url, command.headers);
|
|
406
|
-
}
|
|
407
|
-
await page.goto(command.url, {
|
|
408
|
-
waitUntil: command.waitUntil ?? 'domcontentloaded',
|
|
409
|
-
timeout: command.timeout ?? 30000,
|
|
410
|
-
});
|
|
411
|
-
if (browser.isRecordingSession()) {
|
|
412
|
-
await browser.injectRecorderIfNeeded();
|
|
413
|
-
}
|
|
414
|
-
return successResponse(command.id, {
|
|
415
|
-
url: page.url(),
|
|
416
|
-
title: await page.title(),
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
async function handleClick(command, browser) {
|
|
420
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
421
|
-
const isRef = browser.isRef(command.selector);
|
|
422
|
-
// Quick check: fail fast if element doesn't exist
|
|
423
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
424
|
-
if (command.human?.enabled) {
|
|
425
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
426
|
-
try {
|
|
427
|
-
const page = browser.getPage();
|
|
428
|
-
const box = await locator.boundingBox();
|
|
429
|
-
if (!box) {
|
|
430
|
-
throw new Error(`Element not visible: ${command.selector}`);
|
|
431
|
-
}
|
|
432
|
-
const targetX = box.x + box.width / 2;
|
|
433
|
-
const targetY = box.y + box.height / 2;
|
|
434
|
-
await humanClick(page, targetX, targetY, command.human, {
|
|
435
|
-
button: command.button,
|
|
436
|
-
clickCount: command.clickCount,
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
throw toAIFriendlyError(error, command.selector);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
const result = { clicked: true, human: true };
|
|
444
|
-
if (diffResult) {
|
|
445
|
-
result.diff = diffResult.output;
|
|
446
|
-
result.diffScope = diffResult.diff.scope;
|
|
447
|
-
}
|
|
448
|
-
browser.recordCommand('click', command.selector, undefined, true);
|
|
449
|
-
return successResponse(command.id, result);
|
|
450
|
-
}
|
|
451
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
452
|
-
try {
|
|
453
|
-
await locator.click({
|
|
454
|
-
button: command.button,
|
|
455
|
-
clickCount: command.clickCount,
|
|
456
|
-
delay: command.delay,
|
|
457
|
-
timeout: command.timeout || 5000, // Default 5s timeout for element to appear
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
catch (error) {
|
|
461
|
-
throw toAIFriendlyError(error, command.selector);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
const result = { clicked: true };
|
|
465
|
-
if (diffResult) {
|
|
466
|
-
result.diff = diffResult.output;
|
|
467
|
-
result.diffScope = diffResult.diff.scope;
|
|
468
|
-
}
|
|
469
|
-
browser.recordCommand('click', command.selector, undefined, true);
|
|
470
|
-
return successResponse(command.id, result);
|
|
471
|
-
}
|
|
472
|
-
async function handleType(command, browser) {
|
|
473
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
474
|
-
const isRef = browser.isRef(command.selector);
|
|
475
|
-
// Quick check: fail fast if element doesn't exist
|
|
476
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
477
|
-
if (command.human?.enabled) {
|
|
478
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
479
|
-
try {
|
|
480
|
-
const page = browser.getPage();
|
|
481
|
-
const box = await locator.boundingBox();
|
|
482
|
-
if (!box) {
|
|
483
|
-
throw new Error(`Element not visible: ${command.selector}`);
|
|
484
|
-
}
|
|
485
|
-
const targetX = box.x + box.width / 2;
|
|
486
|
-
const targetY = box.y + box.height / 2;
|
|
487
|
-
await humanClick(page, targetX, targetY, command.human);
|
|
488
|
-
await locator.focus();
|
|
489
|
-
await humanType(page, command.text, command.human);
|
|
490
|
-
}
|
|
491
|
-
catch (error) {
|
|
492
|
-
throw toAIFriendlyError(error, command.selector);
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
const result = { typed: true, human: true };
|
|
496
|
-
if (diffResult) {
|
|
497
|
-
result.diff = diffResult.output;
|
|
498
|
-
result.diffScope = diffResult.diff.scope;
|
|
499
|
-
}
|
|
500
|
-
browser.recordCommand('type', command.selector, command.text, true);
|
|
501
|
-
return successResponse(command.id, result);
|
|
502
|
-
}
|
|
503
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
504
|
-
try {
|
|
505
|
-
if (command.clear) {
|
|
506
|
-
await locator.fill('');
|
|
507
|
-
}
|
|
508
|
-
await locator.pressSequentially(command.text, {
|
|
509
|
-
delay: command.delay,
|
|
510
|
-
});
|
|
511
|
-
await locator.evaluate((el) => {
|
|
512
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
catch (error) {
|
|
516
|
-
throw toAIFriendlyError(error, command.selector);
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
const result = { typed: true };
|
|
520
|
-
if (diffResult) {
|
|
521
|
-
result.diff = diffResult.output;
|
|
522
|
-
result.diffScope = diffResult.diff.scope;
|
|
523
|
-
}
|
|
524
|
-
browser.recordCommand('type', command.selector, command.text, true);
|
|
525
|
-
return successResponse(command.id, result);
|
|
526
|
-
}
|
|
527
|
-
async function handlePress(command, browser) {
|
|
528
|
-
const page = browser.getPage();
|
|
529
|
-
let locator = page.locator('body');
|
|
530
|
-
if (command.inFrame && command.selector) {
|
|
531
|
-
const frameLocator = browser.getFrame(command.inFrame);
|
|
532
|
-
locator = frameLocator.locator(command.selector);
|
|
533
|
-
}
|
|
534
|
-
else if (command.selector) {
|
|
535
|
-
locator = page.locator(command.selector);
|
|
536
|
-
}
|
|
537
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
538
|
-
if (command.inFrame && command.selector) {
|
|
539
|
-
const frameLocator = browser.getFrame(command.inFrame);
|
|
540
|
-
await frameLocator.locator(command.selector).press(command.key);
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
if (command.selector) {
|
|
544
|
-
await page.press(command.selector, command.key);
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
await page.keyboard.press(command.key);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
// 触发 keydown 事件供录制器捕获
|
|
551
|
-
// Playwright 的 press 方法不会触发 JavaScript 层面的 keydown 事件
|
|
552
|
-
// 需要手动触发事件以便录制器能够捕获按键操作
|
|
553
|
-
await page.evaluate((key) => {
|
|
554
|
-
const specialKeys = [
|
|
555
|
-
'Enter',
|
|
556
|
-
'Tab',
|
|
557
|
-
'Escape',
|
|
558
|
-
'Backspace',
|
|
559
|
-
'ArrowUp',
|
|
560
|
-
'ArrowDown',
|
|
561
|
-
'ArrowLeft',
|
|
562
|
-
'ArrowRight',
|
|
563
|
-
];
|
|
564
|
-
const keyParts = key.split('+');
|
|
565
|
-
const mainKey = keyParts[keyParts.length - 1];
|
|
566
|
-
const hasCtrl = keyParts.includes('Control') || keyParts.includes('Ctrl');
|
|
567
|
-
const hasMeta = keyParts.includes('Meta') || keyParts.includes('Command');
|
|
568
|
-
const hasAlt = keyParts.includes('Alt');
|
|
569
|
-
const hasShift = keyParts.includes('Shift');
|
|
570
|
-
if (specialKeys.includes(mainKey) || hasCtrl || hasMeta || hasAlt) {
|
|
571
|
-
const event = new KeyboardEvent('keydown', {
|
|
572
|
-
key: mainKey,
|
|
573
|
-
code: mainKey.length === 1 ? `Key${mainKey.toUpperCase()}` : mainKey,
|
|
574
|
-
ctrlKey: hasCtrl,
|
|
575
|
-
metaKey: hasMeta,
|
|
576
|
-
altKey: hasAlt,
|
|
577
|
-
shiftKey: hasShift,
|
|
578
|
-
bubbles: true,
|
|
579
|
-
});
|
|
580
|
-
document.activeElement?.dispatchEvent(event);
|
|
581
|
-
}
|
|
582
|
-
}, command.key);
|
|
583
|
-
});
|
|
584
|
-
const result = { pressed: true };
|
|
585
|
-
if (diffResult) {
|
|
586
|
-
result.diff = diffResult.output;
|
|
587
|
-
result.diffScope = diffResult.diff.scope;
|
|
588
|
-
}
|
|
589
|
-
return successResponse(command.id, result);
|
|
590
|
-
}
|
|
591
|
-
async function handleScreenshot(command, browser) {
|
|
592
|
-
const page = browser.getPage();
|
|
593
|
-
const options = {
|
|
594
|
-
fullPage: command.fullPage,
|
|
595
|
-
type: command.format ?? 'png',
|
|
596
|
-
};
|
|
597
|
-
if (command.format === 'jpeg' && command.quality !== undefined) {
|
|
598
|
-
options.quality = command.quality;
|
|
599
|
-
}
|
|
600
|
-
let target = page;
|
|
601
|
-
if (command.inFrame) {
|
|
602
|
-
const frameLocator = browser.getFrame(command.inFrame);
|
|
603
|
-
if (command.selector) {
|
|
604
|
-
target = frameLocator.locator(command.selector);
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
// For full frame screenshot, use locator(':root') on the frame locator
|
|
608
|
-
target = frameLocator.locator(':root');
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
else if (command.selector) {
|
|
612
|
-
target = browser.getLocator(command.selector);
|
|
613
|
-
}
|
|
614
|
-
try {
|
|
615
|
-
let savePath = command.path;
|
|
616
|
-
if (!savePath) {
|
|
617
|
-
const ext = command.format === 'jpeg' ? 'jpg' : 'png';
|
|
618
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
619
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
620
|
-
const filename = `screenshot-${timestamp}-${random}.${ext}`;
|
|
621
|
-
const screenshotDir = path.join(getAppDir(), 'tmp', 'screenshots');
|
|
622
|
-
mkdirSync(screenshotDir, { recursive: true });
|
|
623
|
-
savePath = path.join(screenshotDir, filename);
|
|
624
|
-
}
|
|
625
|
-
await target.screenshot({ ...options, path: savePath });
|
|
626
|
-
return successResponse(command.id, { path: savePath });
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
if (command.selector) {
|
|
630
|
-
throw toAIFriendlyError(error, command.selector);
|
|
631
|
-
}
|
|
632
|
-
throw error;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
async function handleSnapshot(command, browser) {
|
|
636
|
-
let effectiveSelector = command.selector;
|
|
637
|
-
let detectionResult = null;
|
|
638
|
-
if (!command.selector) {
|
|
639
|
-
const page = browser.getPage();
|
|
640
|
-
detectionResult = await detectMainContent(page);
|
|
641
|
-
effectiveSelector = detectionResult.selector;
|
|
642
|
-
}
|
|
643
|
-
const snapshot = await browser.getSnapshot({
|
|
644
|
-
interactive: command.interactive,
|
|
645
|
-
cursor: command.cursor,
|
|
646
|
-
maxDepth: command.maxDepth,
|
|
647
|
-
compact: command.compact,
|
|
648
|
-
selector: effectiveSelector,
|
|
649
|
-
framePath: command.inFrame,
|
|
650
|
-
path: command.path,
|
|
651
|
-
attrs: command.attrs,
|
|
652
|
-
selectors: command.selectors,
|
|
653
|
-
all: command.all,
|
|
654
|
-
});
|
|
655
|
-
const simpleRefs = {};
|
|
656
|
-
const refs = snapshot.refs || {};
|
|
657
|
-
for (const [ref, data] of Object.entries(refs)) {
|
|
658
|
-
simpleRefs[ref] = {
|
|
659
|
-
role: data.role,
|
|
660
|
-
name: data.name,
|
|
661
|
-
...(data.xpath && { xpath: data.xpath }),
|
|
662
|
-
...(data.cssPath && { cssPath: data.cssPath }),
|
|
663
|
-
...(data.attributes && { attributes: data.attributes }),
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
// 生成 tips(只有自动检测到非 body 区域时才提示)
|
|
667
|
-
const tips = detectionResult ? generateContentTips(detectionResult) : undefined;
|
|
668
|
-
return successResponse(command.id, {
|
|
669
|
-
snapshot: snapshot.tree || 'Empty page',
|
|
670
|
-
refs: Object.keys(simpleRefs).length > 0 ? simpleRefs : undefined,
|
|
671
|
-
}, tips ?? undefined);
|
|
672
|
-
}
|
|
673
|
-
async function handleEvaluate(command, browser) {
|
|
674
|
-
try {
|
|
675
|
-
let script;
|
|
676
|
-
if (command.file) {
|
|
677
|
-
const resolvedPath = path.resolve(command.file);
|
|
678
|
-
const cwd = process.cwd();
|
|
679
|
-
if (!resolvedPath.startsWith(cwd)) {
|
|
680
|
-
throw new Error(`Security: file path must be within project directory. Got: ${resolvedPath}`);
|
|
681
|
-
}
|
|
682
|
-
if (!existsSync(resolvedPath)) {
|
|
683
|
-
throw new Error(`Script file not found: ${resolvedPath}`);
|
|
684
|
-
}
|
|
685
|
-
script = readFileSync(resolvedPath, 'utf-8');
|
|
686
|
-
}
|
|
687
|
-
else if (command.script) {
|
|
688
|
-
script = command.script;
|
|
689
|
-
}
|
|
690
|
-
else {
|
|
691
|
-
throw new Error('Either script or file must be provided for evaluate command');
|
|
692
|
-
}
|
|
693
|
-
let result;
|
|
694
|
-
if (command.inFrame) {
|
|
695
|
-
const frameLocator = browser.getFrame(command.inFrame);
|
|
696
|
-
result = await frameLocator.locator(':root').evaluate(script);
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
const page = browser.getPage();
|
|
700
|
-
result = await page.evaluate(script);
|
|
701
|
-
}
|
|
702
|
-
browser.recordCommand('eval', 'javascript', script.length > 200 ? script.substring(0, 200) + '...' : script, true);
|
|
703
|
-
return successResponse(command.id, { result });
|
|
704
|
-
}
|
|
705
|
-
catch (error) {
|
|
706
|
-
const script = command.script || command.file || '';
|
|
707
|
-
browser.recordCommand('eval', 'javascript', script.length > 200 ? script.substring(0, 200) + '...' : script, false);
|
|
708
|
-
console.error('Error in handleEvaluate:', error);
|
|
709
|
-
return errorResponse(command.id, error instanceof Error ? error.message : String(error));
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
async function handleWait(command, browser) {
|
|
713
|
-
const humanConfig = getHumanConfigFromEnv();
|
|
714
|
-
const page = browser.getPage();
|
|
715
|
-
// If human mode is enabled and waiting for a duration, do mouse wander
|
|
716
|
-
if (humanConfig.enabled && command.timeout && !command.selector) {
|
|
717
|
-
await humanWander(page, humanConfig, { duration: command.timeout });
|
|
718
|
-
return successResponse(command.id, { waited: true, wandered: true });
|
|
719
|
-
}
|
|
720
|
-
if (command.inFrame) {
|
|
721
|
-
const frame = browser.getFrame(command.inFrame);
|
|
722
|
-
if (command.selector) {
|
|
723
|
-
await frame.waitForSelector(command.selector, {
|
|
724
|
-
state: command.state ?? 'visible',
|
|
725
|
-
timeout: command.timeout,
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
else if (command.timeout) {
|
|
729
|
-
await frame.waitForTimeout(command.timeout);
|
|
730
|
-
}
|
|
731
|
-
else {
|
|
732
|
-
await frame.waitForLoadState('load');
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
const frame = browser.getFrame();
|
|
737
|
-
if (command.selector) {
|
|
738
|
-
await frame.waitForSelector(command.selector, {
|
|
739
|
-
state: command.state ?? 'visible',
|
|
740
|
-
timeout: command.timeout,
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
else if (command.timeout) {
|
|
744
|
-
await frame.waitForTimeout(command.timeout);
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
await frame.waitForLoadState('load');
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
return successResponse(command.id, { waited: true });
|
|
751
|
-
}
|
|
752
|
-
async function handleScroll(command, browser) {
|
|
753
|
-
const page = browser.getPage();
|
|
754
|
-
if (command.selector) {
|
|
755
|
-
const element = page.locator(command.selector);
|
|
756
|
-
await element.scrollIntoViewIfNeeded();
|
|
757
|
-
if (command.x !== undefined || command.y !== undefined) {
|
|
758
|
-
await element.evaluate((el, { x, y }) => {
|
|
759
|
-
if ('scrollBy' in el) {
|
|
760
|
-
el.scrollBy(x ?? 0, y ?? 0);
|
|
761
|
-
}
|
|
762
|
-
}, { x: command.x, y: command.y });
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
// Scroll the page
|
|
767
|
-
let deltaX = command.x ?? 0;
|
|
768
|
-
let deltaY = command.y ?? 0;
|
|
769
|
-
if (command.direction) {
|
|
770
|
-
const amount = command.amount ?? 100;
|
|
771
|
-
switch (command.direction) {
|
|
772
|
-
case 'up':
|
|
773
|
-
deltaY = -amount;
|
|
774
|
-
break;
|
|
775
|
-
case 'down':
|
|
776
|
-
deltaY = amount;
|
|
777
|
-
break;
|
|
778
|
-
case 'left':
|
|
779
|
-
deltaX = -amount;
|
|
780
|
-
break;
|
|
781
|
-
case 'right':
|
|
782
|
-
deltaX = amount;
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const safeDeltaX = Number(deltaX) || 0;
|
|
787
|
-
const safeDeltaY = Number(deltaY) || 0;
|
|
788
|
-
await page.evaluate(({ dx, dy }) => window.scrollBy(dx, dy), {
|
|
789
|
-
dx: safeDeltaX,
|
|
790
|
-
dy: safeDeltaY,
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
return successResponse(command.id, { scrolled: true });
|
|
794
|
-
}
|
|
795
|
-
async function handleSelect(command, browser) {
|
|
796
|
-
const values = Array.isArray(command.values) ? command.values : [command.values];
|
|
797
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
798
|
-
const isRef = browser.isRef(command.selector);
|
|
799
|
-
// Quick check: fail fast if element doesn't exist
|
|
800
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
801
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
802
|
-
try {
|
|
803
|
-
await locator.selectOption(values);
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
throw toAIFriendlyError(error, command.selector);
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
const result = { selected: values };
|
|
810
|
-
if (diffResult) {
|
|
811
|
-
result.diff = diffResult.output;
|
|
812
|
-
result.diffScope = diffResult.diff.scope;
|
|
813
|
-
}
|
|
814
|
-
browser.recordCommand('select', command.selector, values.join(','), true);
|
|
815
|
-
return successResponse(command.id, result);
|
|
816
|
-
}
|
|
817
|
-
async function handleHover(command, browser) {
|
|
818
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
819
|
-
const isRef = browser.isRef(command.selector);
|
|
820
|
-
// Quick check: fail fast if element doesn't exist
|
|
821
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
822
|
-
if (command.human?.enabled) {
|
|
823
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
824
|
-
try {
|
|
825
|
-
const page = browser.getPage();
|
|
826
|
-
const box = await locator.boundingBox();
|
|
827
|
-
if (!box) {
|
|
828
|
-
throw new Error(`Element not visible: ${command.selector}`);
|
|
829
|
-
}
|
|
830
|
-
const targetX = box.x + box.width / 2;
|
|
831
|
-
const targetY = box.y + box.height / 2;
|
|
832
|
-
await humanMoveTo(page, { x: targetX, y: targetY }, command.human);
|
|
833
|
-
}
|
|
834
|
-
catch (error) {
|
|
835
|
-
throw toAIFriendlyError(error, command.selector);
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
const result = { hovered: true, human: true };
|
|
839
|
-
if (diffResult) {
|
|
840
|
-
result.diff = diffResult.output;
|
|
841
|
-
result.diffScope = diffResult.diff.scope;
|
|
842
|
-
}
|
|
843
|
-
return successResponse(command.id, result);
|
|
844
|
-
}
|
|
845
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
846
|
-
try {
|
|
847
|
-
await locator.hover();
|
|
848
|
-
}
|
|
849
|
-
catch (error) {
|
|
850
|
-
throw toAIFriendlyError(error, command.selector);
|
|
851
|
-
}
|
|
852
|
-
});
|
|
853
|
-
const result = { hovered: true };
|
|
854
|
-
if (diffResult) {
|
|
855
|
-
result.diff = diffResult.output;
|
|
856
|
-
result.diffScope = diffResult.diff.scope;
|
|
857
|
-
}
|
|
858
|
-
return successResponse(command.id, result);
|
|
859
|
-
}
|
|
860
|
-
async function handleContent(command, browser) {
|
|
861
|
-
const page = browser.getPage();
|
|
862
|
-
let html;
|
|
863
|
-
if (command.selector) {
|
|
864
|
-
html = await page.locator(command.selector).innerHTML();
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
html = await page.content();
|
|
868
|
-
}
|
|
869
|
-
return successResponse(command.id, { html });
|
|
870
|
-
}
|
|
871
|
-
async function handleClose(command, browser) {
|
|
872
|
-
await browser.close();
|
|
873
|
-
return successResponse(command.id, { closed: true });
|
|
874
|
-
}
|
|
875
|
-
async function handleTabNew(command, browser) {
|
|
876
|
-
const result = await browser.newTab();
|
|
877
|
-
// Navigate to URL if provided (same pattern as handleNavigate)
|
|
878
|
-
if (command.url) {
|
|
879
|
-
const page = browser.getPage();
|
|
880
|
-
await page.goto(command.url, { waitUntil: 'domcontentloaded' });
|
|
881
|
-
}
|
|
882
|
-
return successResponse(command.id, result);
|
|
883
|
-
}
|
|
884
|
-
async function handleTabList(command, browser) {
|
|
885
|
-
const tabs = await browser.listTabs();
|
|
886
|
-
return successResponse(command.id, {
|
|
887
|
-
tabs,
|
|
888
|
-
active: browser.getActiveIndex(),
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
async function handleFrames(command, browser) {
|
|
892
|
-
const frames = browser.listFrames();
|
|
893
|
-
if (frames.length === 0) {
|
|
894
|
-
return successResponse(command.id, {
|
|
895
|
-
frames: [],
|
|
896
|
-
tip: 'No iframes found on this page.',
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
return successResponse(command.id, { frames });
|
|
900
|
-
}
|
|
901
|
-
async function handleTabSwitch(command, browser) {
|
|
902
|
-
const result = await browser.switchTo(command.index);
|
|
903
|
-
const page = browser.getPage();
|
|
904
|
-
return successResponse(command.id, {
|
|
905
|
-
...result,
|
|
906
|
-
title: await page.title(),
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
async function handleTabClose(command, browser) {
|
|
910
|
-
const result = await browser.closeTab(command.index);
|
|
911
|
-
return successResponse(command.id, result);
|
|
912
|
-
}
|
|
913
|
-
async function handleWindowNew(command, browser) {
|
|
914
|
-
const result = await browser.newWindow(command.viewport);
|
|
915
|
-
return successResponse(command.id, result);
|
|
916
|
-
}
|
|
917
|
-
// New handlers for enhanced Playwright parity
|
|
918
|
-
async function handleFill(command, browser) {
|
|
919
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
920
|
-
const isRef = browser.isRef(command.selector);
|
|
921
|
-
// Quick check: fail fast if element doesn't exist
|
|
922
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
923
|
-
if (command.human?.enabled) {
|
|
924
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
925
|
-
try {
|
|
926
|
-
const page = browser.getPage();
|
|
927
|
-
const box = await locator.boundingBox();
|
|
928
|
-
if (!box) {
|
|
929
|
-
throw new Error(`Element not visible: ${command.selector}`);
|
|
930
|
-
}
|
|
931
|
-
const targetX = box.x + box.width / 2;
|
|
932
|
-
const targetY = box.y + box.height / 2;
|
|
933
|
-
await humanClick(page, targetX, targetY, command.human);
|
|
934
|
-
await locator.focus();
|
|
935
|
-
// Clear existing content: triple-click to select all, then delete with Backspace
|
|
936
|
-
await page.mouse.click(targetX, targetY, { clickCount: 3 });
|
|
937
|
-
await page.keyboard.press('Backspace');
|
|
938
|
-
await humanType(page, command.value, command.human);
|
|
939
|
-
}
|
|
940
|
-
catch (error) {
|
|
941
|
-
throw toAIFriendlyError(error, command.selector);
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
const result = { filled: true, human: true };
|
|
945
|
-
if (diffResult) {
|
|
946
|
-
result.diff = diffResult.output;
|
|
947
|
-
result.diffScope = diffResult.diff.scope;
|
|
948
|
-
}
|
|
949
|
-
browser.recordCommand('fill', command.selector, command.value, true);
|
|
950
|
-
return successResponse(command.id, result);
|
|
951
|
-
}
|
|
952
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
953
|
-
try {
|
|
954
|
-
await locator.fill(command.value);
|
|
955
|
-
if (!isRef) {
|
|
956
|
-
const page = browser.getPage();
|
|
957
|
-
if (page) {
|
|
958
|
-
await page.evaluate((selector) => {
|
|
959
|
-
const el = document.querySelector(selector);
|
|
960
|
-
if (el) {
|
|
961
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
962
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
963
|
-
}
|
|
964
|
-
}, command.selector);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
catch (error) {
|
|
969
|
-
throw toAIFriendlyError(error, command.selector);
|
|
970
|
-
}
|
|
971
|
-
});
|
|
972
|
-
const result = { filled: true };
|
|
973
|
-
if (diffResult) {
|
|
974
|
-
result.diff = diffResult.output;
|
|
975
|
-
result.diffScope = diffResult.diff.scope;
|
|
976
|
-
}
|
|
977
|
-
browser.recordCommand('fill', command.selector, command.value, true);
|
|
978
|
-
return successResponse(command.id, result);
|
|
979
|
-
}
|
|
980
|
-
async function handleCheck(command, browser) {
|
|
981
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
982
|
-
const isRef = browser.isRef(command.selector);
|
|
983
|
-
// Quick check: fail fast if element doesn't exist
|
|
984
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
985
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
986
|
-
try {
|
|
987
|
-
await locator.check();
|
|
988
|
-
await locator.evaluate((el) => {
|
|
989
|
-
el.dispatchEvent(new Event('click', { bubbles: true }));
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
catch (error) {
|
|
993
|
-
throw toAIFriendlyError(error, command.selector);
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
const result = { checked: true };
|
|
997
|
-
if (diffResult) {
|
|
998
|
-
result.diff = diffResult.output;
|
|
999
|
-
result.diffScope = diffResult.diff.scope;
|
|
1000
|
-
}
|
|
1001
|
-
browser.recordCommand('check', command.selector, undefined, true);
|
|
1002
|
-
return successResponse(command.id, result);
|
|
1003
|
-
}
|
|
1004
|
-
async function handleUncheck(command, browser) {
|
|
1005
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1006
|
-
const isRef = browser.isRef(command.selector);
|
|
1007
|
-
// Quick check: fail fast if element doesn't exist
|
|
1008
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
1009
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
1010
|
-
try {
|
|
1011
|
-
await locator.uncheck();
|
|
1012
|
-
await locator.evaluate((el) => {
|
|
1013
|
-
el.dispatchEvent(new Event('click', { bubbles: true }));
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
catch (error) {
|
|
1017
|
-
throw toAIFriendlyError(error, command.selector);
|
|
1018
|
-
}
|
|
1019
|
-
});
|
|
1020
|
-
const result = { unchecked: true };
|
|
1021
|
-
if (diffResult) {
|
|
1022
|
-
result.diff = diffResult.output;
|
|
1023
|
-
result.diffScope = diffResult.diff.scope;
|
|
1024
|
-
}
|
|
1025
|
-
browser.recordCommand('uncheck', command.selector, undefined, true);
|
|
1026
|
-
return successResponse(command.id, result);
|
|
1027
|
-
}
|
|
1028
|
-
async function handleUpload(command, browser) {
|
|
1029
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1030
|
-
const isRef = browser.isRef(command.selector);
|
|
1031
|
-
// Quick check: fail fast if element doesn't exist
|
|
1032
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
1033
|
-
const files = Array.isArray(command.files) ? command.files : [command.files];
|
|
1034
|
-
try {
|
|
1035
|
-
await locator.setInputFiles(files);
|
|
1036
|
-
}
|
|
1037
|
-
catch (error) {
|
|
1038
|
-
throw toAIFriendlyError(error, command.selector);
|
|
1039
|
-
}
|
|
1040
|
-
return successResponse(command.id, { uploaded: files });
|
|
1041
|
-
}
|
|
1042
|
-
async function handleDoubleClick(command, browser) {
|
|
1043
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1044
|
-
const isRef = browser.isRef(command.selector);
|
|
1045
|
-
// Quick check: fail fast if element doesn't exist
|
|
1046
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
1047
|
-
if (command.human?.enabled) {
|
|
1048
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
1049
|
-
try {
|
|
1050
|
-
const page = browser.getPage();
|
|
1051
|
-
const box = await locator.boundingBox();
|
|
1052
|
-
if (!box) {
|
|
1053
|
-
throw new Error(`Element not visible: ${command.selector}`);
|
|
1054
|
-
}
|
|
1055
|
-
const targetX = box.x + box.width / 2;
|
|
1056
|
-
const targetY = box.y + box.height / 2;
|
|
1057
|
-
await humanClick(page, targetX, targetY, command.human, {
|
|
1058
|
-
clickCount: 2,
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
catch (error) {
|
|
1062
|
-
throw toAIFriendlyError(error, command.selector);
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
const result = { clicked: true, human: true };
|
|
1066
|
-
if (diffResult) {
|
|
1067
|
-
result.diff = diffResult.output;
|
|
1068
|
-
result.diffScope = diffResult.diff.scope;
|
|
1069
|
-
}
|
|
1070
|
-
return successResponse(command.id, result);
|
|
1071
|
-
}
|
|
1072
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
1073
|
-
try {
|
|
1074
|
-
await locator.dblclick();
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
throw toAIFriendlyError(error, command.selector);
|
|
1078
|
-
}
|
|
1079
|
-
});
|
|
1080
|
-
const result = { clicked: true };
|
|
1081
|
-
if (diffResult) {
|
|
1082
|
-
result.diff = diffResult.output;
|
|
1083
|
-
result.diffScope = diffResult.diff.scope;
|
|
1084
|
-
}
|
|
1085
|
-
return successResponse(command.id, result);
|
|
1086
|
-
}
|
|
1087
|
-
async function handleFocus(command, browser) {
|
|
1088
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1089
|
-
const isRef = browser.isRef(command.selector);
|
|
1090
|
-
// Quick check: fail fast if element doesn't exist
|
|
1091
|
-
await assertElementExists(locator, command.selector, isRef);
|
|
1092
|
-
const diffResult = await performDiff(locator, command.diffScope, async () => {
|
|
1093
|
-
try {
|
|
1094
|
-
await locator.focus({ timeout: 5000 });
|
|
1095
|
-
}
|
|
1096
|
-
catch (error) {
|
|
1097
|
-
throw toAIFriendlyError(error, command.selector);
|
|
1098
|
-
}
|
|
1099
|
-
});
|
|
1100
|
-
const result = { focused: true };
|
|
1101
|
-
if (diffResult) {
|
|
1102
|
-
result.diff = diffResult.output;
|
|
1103
|
-
result.diffScope = diffResult.diff.scope;
|
|
1104
|
-
}
|
|
1105
|
-
return successResponse(command.id, result);
|
|
1106
|
-
}
|
|
1107
|
-
async function handleDrag(command, browser) {
|
|
1108
|
-
const sourceLocator = browser.getLocator(command.source, command.inFrame);
|
|
1109
|
-
const targetLocator = browser.getLocator(command.target, command.inFrame);
|
|
1110
|
-
// Quick check: fail fast if elements don't exist
|
|
1111
|
-
const isSourceRef = browser.isRef(command.source);
|
|
1112
|
-
const isTargetRef = browser.isRef(command.target);
|
|
1113
|
-
await assertElementExists(sourceLocator, command.source, isSourceRef);
|
|
1114
|
-
await assertElementExists(targetLocator, command.target, isTargetRef);
|
|
1115
|
-
await sourceLocator.dragTo(targetLocator);
|
|
1116
|
-
return successResponse(command.id, { dragged: true });
|
|
1117
|
-
}
|
|
1118
|
-
async function handleGetByRole(command, browser) {
|
|
1119
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1120
|
-
const locator = frame.getByRole(command.role, {
|
|
1121
|
-
name: command.name,
|
|
1122
|
-
exact: command.exact,
|
|
1123
|
-
});
|
|
1124
|
-
try {
|
|
1125
|
-
switch (command.subaction) {
|
|
1126
|
-
case 'click':
|
|
1127
|
-
await locator.click();
|
|
1128
|
-
return successResponse(command.id, { clicked: true });
|
|
1129
|
-
case 'fill':
|
|
1130
|
-
await locator.fill(command.value ?? '');
|
|
1131
|
-
return successResponse(command.id, { filled: true });
|
|
1132
|
-
case 'check':
|
|
1133
|
-
await locator.check();
|
|
1134
|
-
return successResponse(command.id, { checked: true });
|
|
1135
|
-
case 'hover':
|
|
1136
|
-
await locator.hover();
|
|
1137
|
-
return successResponse(command.id, { hovered: true });
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
catch (error) {
|
|
1141
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1142
|
-
if (msg.includes('strict mode violation')) {
|
|
1143
|
-
const countMatch = msg.match(/resolved to (\d+) elements/);
|
|
1144
|
-
const count = countMatch ? countMatch[1] : 'multiple';
|
|
1145
|
-
const first = locator.first();
|
|
1146
|
-
const warning = `Matched ${count} elements, used first match. Use 'find nth <index> role "${command.role}" --click' for a specific match.`;
|
|
1147
|
-
switch (command.subaction) {
|
|
1148
|
-
case 'click':
|
|
1149
|
-
await first.click();
|
|
1150
|
-
return successResponse(command.id, { clicked: true, warning });
|
|
1151
|
-
case 'fill':
|
|
1152
|
-
await first.fill(command.value ?? '');
|
|
1153
|
-
return successResponse(command.id, { filled: true, warning });
|
|
1154
|
-
case 'check':
|
|
1155
|
-
await first.check();
|
|
1156
|
-
return successResponse(command.id, { checked: true, warning });
|
|
1157
|
-
case 'hover':
|
|
1158
|
-
await first.hover();
|
|
1159
|
-
return successResponse(command.id, { hovered: true, warning });
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
throw error;
|
|
1163
|
-
}
|
|
1164
|
-
return successResponse(command.id, {});
|
|
1165
|
-
}
|
|
1166
|
-
async function handleGetByText(command, browser) {
|
|
1167
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1168
|
-
const locator = frame.getByText(command.text, { exact: command.exact });
|
|
1169
|
-
try {
|
|
1170
|
-
switch (command.subaction) {
|
|
1171
|
-
case 'click':
|
|
1172
|
-
await locator.click();
|
|
1173
|
-
return successResponse(command.id, { clicked: true });
|
|
1174
|
-
case 'hover':
|
|
1175
|
-
await locator.hover();
|
|
1176
|
-
return successResponse(command.id, { hovered: true });
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
catch (error) {
|
|
1180
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1181
|
-
if (msg.includes('strict mode violation')) {
|
|
1182
|
-
const countMatch = msg.match(/resolved to (\d+) elements/);
|
|
1183
|
-
const count = countMatch ? countMatch[1] : 'multiple';
|
|
1184
|
-
const first = locator.first();
|
|
1185
|
-
switch (command.subaction) {
|
|
1186
|
-
case 'click':
|
|
1187
|
-
await first.click();
|
|
1188
|
-
return successResponse(command.id, {
|
|
1189
|
-
clicked: true,
|
|
1190
|
-
warning: `Matched ${count} elements, used first match. Use 'find nth <index> text "${command.text}" --click' for a specific match.`,
|
|
1191
|
-
});
|
|
1192
|
-
case 'hover':
|
|
1193
|
-
await first.hover();
|
|
1194
|
-
return successResponse(command.id, {
|
|
1195
|
-
hovered: true,
|
|
1196
|
-
warning: `Matched ${count} elements, used first match. Use 'find nth <index> text "${command.text}" --hover' for a specific match.`,
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
throw error;
|
|
1201
|
-
}
|
|
1202
|
-
return successResponse(command.id, {});
|
|
1203
|
-
}
|
|
1204
|
-
async function handleGetByLabel(command, browser) {
|
|
1205
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1206
|
-
const locator = frame.getByLabel(command.label, { exact: command.exact });
|
|
1207
|
-
switch (command.subaction) {
|
|
1208
|
-
case 'click':
|
|
1209
|
-
await locator.click();
|
|
1210
|
-
return successResponse(command.id, { clicked: true });
|
|
1211
|
-
case 'fill':
|
|
1212
|
-
await locator.fill(command.value ?? '');
|
|
1213
|
-
return successResponse(command.id, { filled: true });
|
|
1214
|
-
case 'check':
|
|
1215
|
-
await locator.check();
|
|
1216
|
-
return successResponse(command.id, { checked: true });
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
async function handleGetByPlaceholder(command, browser) {
|
|
1220
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1221
|
-
const locator = frame.getByPlaceholder(command.placeholder, { exact: command.exact });
|
|
1222
|
-
switch (command.subaction) {
|
|
1223
|
-
case 'click':
|
|
1224
|
-
await locator.click();
|
|
1225
|
-
return successResponse(command.id, { clicked: true });
|
|
1226
|
-
case 'fill':
|
|
1227
|
-
await locator.fill(command.value ?? '');
|
|
1228
|
-
return successResponse(command.id, { filled: true });
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
async function handleCookiesGet(command, browser) {
|
|
1232
|
-
const page = browser.getPage();
|
|
1233
|
-
const context = page.context();
|
|
1234
|
-
const cookies = await context.cookies(command.urls);
|
|
1235
|
-
return successResponse(command.id, { cookies });
|
|
1236
|
-
}
|
|
1237
|
-
async function handleCookiesSet(command, browser) {
|
|
1238
|
-
const page = browser.getPage();
|
|
1239
|
-
const context = page.context();
|
|
1240
|
-
// Auto-fill URL for cookies that don't have domain/path/url set
|
|
1241
|
-
const pageUrl = page.url();
|
|
1242
|
-
const cookies = command.cookies.map((cookie) => {
|
|
1243
|
-
if (!cookie.url && !cookie.domain && !cookie.path) {
|
|
1244
|
-
return { ...cookie, url: pageUrl };
|
|
1245
|
-
}
|
|
1246
|
-
return cookie;
|
|
1247
|
-
});
|
|
1248
|
-
await context.addCookies(cookies);
|
|
1249
|
-
return successResponse(command.id, { set: true });
|
|
1250
|
-
}
|
|
1251
|
-
async function handleCookiesClear(command, browser) {
|
|
1252
|
-
const page = browser.getPage();
|
|
1253
|
-
const context = page.context();
|
|
1254
|
-
await context.clearCookies();
|
|
1255
|
-
return successResponse(command.id, { cleared: true });
|
|
1256
|
-
}
|
|
1257
|
-
async function handleStorageGet(command, browser) {
|
|
1258
|
-
const frame = browser.getFrame();
|
|
1259
|
-
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
|
1260
|
-
if (command.key) {
|
|
1261
|
-
const value = await frame.evaluate(`
|
|
1262
|
-
${storageType}.getItem(${JSON.stringify(command.key)})
|
|
1263
|
-
`);
|
|
1264
|
-
return successResponse(command.id, { key: command.key, value });
|
|
1265
|
-
}
|
|
1266
|
-
else {
|
|
1267
|
-
const data = await frame.evaluate(`
|
|
1268
|
-
(() => {
|
|
1269
|
-
const storage = ${storageType};
|
|
1270
|
-
const result = {};
|
|
1271
|
-
for (let i = 0; i < storage.length; i++) {
|
|
1272
|
-
const key = storage.key(i);
|
|
1273
|
-
if (key) result[key] = storage.getItem(key);
|
|
1274
|
-
}
|
|
1275
|
-
return result;
|
|
1276
|
-
})()
|
|
1277
|
-
`);
|
|
1278
|
-
return successResponse(command.id, { data });
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
async function handleStorageSet(command, browser) {
|
|
1282
|
-
const frame = browser.getFrame();
|
|
1283
|
-
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
|
1284
|
-
await frame.evaluate(`${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})`);
|
|
1285
|
-
return successResponse(command.id, { set: true });
|
|
1286
|
-
}
|
|
1287
|
-
async function handleStorageClear(command, browser) {
|
|
1288
|
-
const frame = browser.getFrame();
|
|
1289
|
-
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
|
1290
|
-
await frame.evaluate(`${storageType}.clear()`);
|
|
1291
|
-
return successResponse(command.id, { cleared: true });
|
|
1292
|
-
}
|
|
1293
|
-
async function handleDialog(command, browser) {
|
|
1294
|
-
browser.setDialogHandler(command.response, command.promptText);
|
|
1295
|
-
return successResponse(command.id, { handler: 'set', response: command.response });
|
|
1296
|
-
}
|
|
1297
|
-
async function handlePdf(command, browser) {
|
|
1298
|
-
const page = browser.getPage();
|
|
1299
|
-
await page.pdf({
|
|
1300
|
-
path: command.path,
|
|
1301
|
-
format: command.format ?? 'Letter',
|
|
1302
|
-
});
|
|
1303
|
-
return successResponse(command.id, { path: command.path });
|
|
1304
|
-
}
|
|
1305
|
-
// Network & Request handlers
|
|
1306
|
-
async function handleRoute(command, browser) {
|
|
1307
|
-
await browser.addRoute(command.url, {
|
|
1308
|
-
response: command.response,
|
|
1309
|
-
abort: command.abort,
|
|
1310
|
-
});
|
|
1311
|
-
return successResponse(command.id, { routed: command.url });
|
|
1312
|
-
}
|
|
1313
|
-
async function handleUnroute(command, browser) {
|
|
1314
|
-
await browser.removeRoute(command.url);
|
|
1315
|
-
return successResponse(command.id, { unrouted: command.url ?? 'all' });
|
|
1316
|
-
}
|
|
1317
|
-
async function handleRequests(command, browser) {
|
|
1318
|
-
if (command.clear) {
|
|
1319
|
-
browser.clearRequests();
|
|
1320
|
-
return successResponse(command.id, { cleared: true });
|
|
1321
|
-
}
|
|
1322
|
-
// Start tracking if not already (with response capture if requested)
|
|
1323
|
-
const wasTracking = browser.trackingEnabled;
|
|
1324
|
-
browser.startRequestTracking(command.captureResponse);
|
|
1325
|
-
// If output directory is specified, save to directory
|
|
1326
|
-
if (command.output) {
|
|
1327
|
-
const result = browser.saveRequestsToDir(command.output, command.filter, command.type);
|
|
1328
|
-
return successResponse(command.id, {
|
|
1329
|
-
saved: true,
|
|
1330
|
-
savedCount: result.savedCount,
|
|
1331
|
-
outputPath: result.outputPath,
|
|
1332
|
-
indexPath: result.indexPath,
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
const requests = browser.getRequests(command.filter, command.type);
|
|
1336
|
-
const result = { requests };
|
|
1337
|
-
if (requests.length === 0 && !wasTracking) {
|
|
1338
|
-
result.hint = 'Request tracking just activated. Reload or navigate to capture requests.';
|
|
1339
|
-
}
|
|
1340
|
-
return successResponse(command.id, result);
|
|
1341
|
-
}
|
|
1342
|
-
async function handleWebSockets(command, browser) {
|
|
1343
|
-
if (command.clear) {
|
|
1344
|
-
browser.clearWebSockets();
|
|
1345
|
-
return successResponse(command.id, { cleared: true });
|
|
1346
|
-
}
|
|
1347
|
-
const wasTracking = browser.wsTrackingEnabled;
|
|
1348
|
-
browser.startWebSocketTracking();
|
|
1349
|
-
const sockets = browser.getWebSockets(command.filter);
|
|
1350
|
-
const result = { websockets: sockets };
|
|
1351
|
-
if (sockets.length === 0 && !wasTracking) {
|
|
1352
|
-
result.hint = 'WebSocket tracking just activated. Reload or navigate to capture connections.';
|
|
1353
|
-
}
|
|
1354
|
-
return successResponse(command.id, result);
|
|
1355
|
-
}
|
|
1356
|
-
async function handleDownload(command, browser) {
|
|
1357
|
-
const page = browser.getPage();
|
|
1358
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1359
|
-
const [download] = await Promise.all([page.waitForEvent('download'), locator.click()]);
|
|
1360
|
-
await download.saveAs(command.path);
|
|
1361
|
-
return successResponse(command.id, {
|
|
1362
|
-
path: command.path,
|
|
1363
|
-
suggestedFilename: download.suggestedFilename(),
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
async function handleGeolocation(command, browser) {
|
|
1367
|
-
await browser.setGeolocation(command.latitude, command.longitude, command.accuracy);
|
|
1368
|
-
return successResponse(command.id, {
|
|
1369
|
-
latitude: command.latitude,
|
|
1370
|
-
longitude: command.longitude,
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
async function handlePermissions(command, browser) {
|
|
1374
|
-
await browser.setPermissions(command.permissions, command.grant);
|
|
1375
|
-
return successResponse(command.id, {
|
|
1376
|
-
permissions: command.permissions,
|
|
1377
|
-
granted: command.grant,
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
async function handleViewport(command, browser) {
|
|
1381
|
-
await browser.setViewport(command.width, command.height);
|
|
1382
|
-
return successResponse(command.id, {
|
|
1383
|
-
width: command.width,
|
|
1384
|
-
height: command.height,
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
async function handleUserAgent(command, browser) {
|
|
1388
|
-
const page = browser.getPage();
|
|
1389
|
-
const context = page.context();
|
|
1390
|
-
// Note: Can't change user agent after context is created, but we can for new pages
|
|
1391
|
-
return successResponse(command.id, {
|
|
1392
|
-
note: 'User agent can only be set at launch time. Use device command instead.',
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
async function handleDevice(command, browser) {
|
|
1396
|
-
const device = browser.getDevice(command.device);
|
|
1397
|
-
if (!device) {
|
|
1398
|
-
const available = browser.listDevices().slice(0, 10).join(', ');
|
|
1399
|
-
throw new Error(`Unknown device: ${command.device}. Available: ${available}...`);
|
|
1400
|
-
}
|
|
1401
|
-
// Apply device viewport
|
|
1402
|
-
await browser.setViewport(device.viewport.width, device.viewport.height);
|
|
1403
|
-
// Apply or clear device scale factor
|
|
1404
|
-
if (device.deviceScaleFactor && device.deviceScaleFactor !== 1) {
|
|
1405
|
-
// Apply device scale factor for HiDPI/retina displays
|
|
1406
|
-
await browser.setDeviceScaleFactor(device.deviceScaleFactor, device.viewport.width, device.viewport.height, device.isMobile ?? false);
|
|
1407
|
-
}
|
|
1408
|
-
else {
|
|
1409
|
-
// Clear device scale factor override to restore default (1x)
|
|
1410
|
-
try {
|
|
1411
|
-
await browser.clearDeviceMetricsOverride();
|
|
1412
|
-
}
|
|
1413
|
-
catch {
|
|
1414
|
-
// Ignore error if override was never set
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
return successResponse(command.id, {
|
|
1418
|
-
device: command.device,
|
|
1419
|
-
viewport: device.viewport,
|
|
1420
|
-
userAgent: device.userAgent,
|
|
1421
|
-
deviceScaleFactor: device.deviceScaleFactor,
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
async function handleBack(command, browser) {
|
|
1425
|
-
browser.recordStep({ action: 'back' });
|
|
1426
|
-
const page = browser.getPage();
|
|
1427
|
-
await page.goBack();
|
|
1428
|
-
return successResponse(command.id, { url: page.url() });
|
|
1429
|
-
}
|
|
1430
|
-
async function handleForward(command, browser) {
|
|
1431
|
-
browser.recordStep({ action: 'forward' });
|
|
1432
|
-
const page = browser.getPage();
|
|
1433
|
-
await page.goForward();
|
|
1434
|
-
return successResponse(command.id, { url: page.url() });
|
|
1435
|
-
}
|
|
1436
|
-
async function handleReload(command, browser) {
|
|
1437
|
-
browser.recordStep({ action: 'reload' });
|
|
1438
|
-
const page = browser.getPage();
|
|
1439
|
-
await page.reload();
|
|
1440
|
-
return successResponse(command.id, { url: page.url() });
|
|
1441
|
-
}
|
|
1442
|
-
async function handleUrl(command, browser) {
|
|
1443
|
-
if (command.inFrame) {
|
|
1444
|
-
const frameLocator = browser.getFrame(command.inFrame);
|
|
1445
|
-
// Get URL from frame by evaluating JavaScript on root locator
|
|
1446
|
-
const url = await frameLocator.locator(':root').evaluate(() => window.location.href);
|
|
1447
|
-
return successResponse(command.id, { url });
|
|
1448
|
-
}
|
|
1449
|
-
else {
|
|
1450
|
-
const page = browser.getPage();
|
|
1451
|
-
return successResponse(command.id, { url: page.url() });
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
async function handleTitle(command, browser) {
|
|
1455
|
-
if (command.inFrame) {
|
|
1456
|
-
const frameLocator = browser.getFrame(command.inFrame);
|
|
1457
|
-
// Get title from frame by evaluating JavaScript on root locator
|
|
1458
|
-
const title = await frameLocator.locator(':root').evaluate(() => document.title);
|
|
1459
|
-
return successResponse(command.id, { title });
|
|
1460
|
-
}
|
|
1461
|
-
else {
|
|
1462
|
-
const page = browser.getPage();
|
|
1463
|
-
const title = await page.title();
|
|
1464
|
-
return successResponse(command.id, { title });
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
async function handleGetAttribute(command, browser) {
|
|
1468
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1469
|
-
const value = await locator.getAttribute(command.attribute);
|
|
1470
|
-
return successResponse(command.id, { attribute: command.attribute, value });
|
|
1471
|
-
}
|
|
1472
|
-
async function handleGetText(command, browser) {
|
|
1473
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1474
|
-
const text = await locator.textContent();
|
|
1475
|
-
return successResponse(command.id, { text });
|
|
1476
|
-
}
|
|
1477
|
-
async function handleIsVisible(command, browser) {
|
|
1478
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1479
|
-
const visible = await locator.isVisible({ timeout: 5000 });
|
|
1480
|
-
return successResponse(command.id, { visible });
|
|
1481
|
-
}
|
|
1482
|
-
async function handleIsEnabled(command, browser) {
|
|
1483
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1484
|
-
const enabled = await locator.isEnabled({ timeout: 5000 });
|
|
1485
|
-
return successResponse(command.id, { enabled });
|
|
1486
|
-
}
|
|
1487
|
-
async function handleIsChecked(command, browser) {
|
|
1488
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1489
|
-
const checked = await locator.isChecked({ timeout: 5000 });
|
|
1490
|
-
return successResponse(command.id, { checked });
|
|
1491
|
-
}
|
|
1492
|
-
async function handleCount(command, browser) {
|
|
1493
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1494
|
-
const count = await locator.count();
|
|
1495
|
-
return successResponse(command.id, { count });
|
|
1496
|
-
}
|
|
1497
|
-
async function handleBoundingBox(command, browser) {
|
|
1498
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1499
|
-
const box = await locator.boundingBox();
|
|
1500
|
-
return successResponse(command.id, { box });
|
|
1501
|
-
}
|
|
1502
|
-
async function handleStyles(command, browser) {
|
|
1503
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1504
|
-
// Shared extraction logic as a string to be eval'd in browser context
|
|
1505
|
-
const extractStylesScript = `(function(el) {
|
|
1506
|
-
const s = getComputedStyle(el);
|
|
1507
|
-
const r = el.getBoundingClientRect();
|
|
1508
|
-
return {
|
|
1509
|
-
tag: el.tagName.toLowerCase(),
|
|
1510
|
-
text: el.innerText?.trim().slice(0, 80) || null,
|
|
1511
|
-
box: {
|
|
1512
|
-
x: Math.round(r.x),
|
|
1513
|
-
y: Math.round(r.y),
|
|
1514
|
-
width: Math.round(r.width),
|
|
1515
|
-
height: Math.round(r.height),
|
|
1516
|
-
},
|
|
1517
|
-
styles: {
|
|
1518
|
-
fontSize: s.fontSize,
|
|
1519
|
-
fontWeight: s.fontWeight,
|
|
1520
|
-
fontFamily: s.fontFamily.split(',')[0].trim().replace(/"/g, ''),
|
|
1521
|
-
color: s.color,
|
|
1522
|
-
backgroundColor: s.backgroundColor,
|
|
1523
|
-
borderRadius: s.borderRadius,
|
|
1524
|
-
border: s.border !== 'none' && s.borderWidth !== '0px' ? s.border : null,
|
|
1525
|
-
boxShadow: s.boxShadow !== 'none' ? s.boxShadow : null,
|
|
1526
|
-
padding: s.padding,
|
|
1527
|
-
},
|
|
1528
|
-
};
|
|
1529
|
-
})`;
|
|
1530
|
-
// Check if it's a ref - single element
|
|
1531
|
-
if (browser.isRef(command.selector)) {
|
|
1532
|
-
const locator = browser.getLocator(command.selector);
|
|
1533
|
-
const element = (await locator.evaluate((el, script) => {
|
|
1534
|
-
const fn = new Function('return ' + script)();
|
|
1535
|
-
return fn(el);
|
|
1536
|
-
}, extractStylesScript));
|
|
1537
|
-
return successResponse(command.id, { elements: [element] });
|
|
1538
|
-
}
|
|
1539
|
-
// CSS selector - can match multiple elements
|
|
1540
|
-
const elements = (await frame.locator(command.selector).evaluateAll((els, script) => {
|
|
1541
|
-
const fn = new Function('return ' + script)();
|
|
1542
|
-
return els.map((el) => fn(el));
|
|
1543
|
-
}, extractStylesScript));
|
|
1544
|
-
return successResponse(command.id, { elements });
|
|
1545
|
-
}
|
|
1546
|
-
// Advanced handlers
|
|
1547
|
-
async function handleVideoStart(command, browser) {
|
|
1548
|
-
// Video recording requires context-level setup at launch
|
|
1549
|
-
// For now, return a note about this limitation
|
|
1550
|
-
return successResponse(command.id, {
|
|
1551
|
-
note: 'Video recording must be enabled at browser launch. Use --video flag when starting.',
|
|
1552
|
-
path: command.path,
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
async function handleVideoStop(command, browser) {
|
|
1556
|
-
const page = browser.getPage();
|
|
1557
|
-
const video = page.video();
|
|
1558
|
-
if (video) {
|
|
1559
|
-
const path = await video.path();
|
|
1560
|
-
return successResponse(command.id, { path });
|
|
1561
|
-
}
|
|
1562
|
-
return successResponse(command.id, { note: 'No video recording active' });
|
|
1563
|
-
}
|
|
1564
|
-
async function handleTraceStart(command, browser) {
|
|
1565
|
-
await browser.startTracing({
|
|
1566
|
-
screenshots: command.screenshots,
|
|
1567
|
-
snapshots: command.snapshots,
|
|
1568
|
-
});
|
|
1569
|
-
return successResponse(command.id, { started: true });
|
|
1570
|
-
}
|
|
1571
|
-
async function handleTraceStop(command, browser) {
|
|
1572
|
-
await browser.stopTracing(command.path);
|
|
1573
|
-
return successResponse(command.id, { path: command.path });
|
|
1574
|
-
}
|
|
1575
|
-
async function handleHarStart(command, browser) {
|
|
1576
|
-
await browser.startHarRecording();
|
|
1577
|
-
browser.startRequestTracking();
|
|
1578
|
-
return successResponse(command.id, { started: true });
|
|
1579
|
-
}
|
|
1580
|
-
async function handleHarStop(command, browser) {
|
|
1581
|
-
// HAR recording is handled at context level
|
|
1582
|
-
// For now, we save tracked requests as a simplified HAR-like format
|
|
1583
|
-
const requests = browser.getRequests();
|
|
1584
|
-
return successResponse(command.id, {
|
|
1585
|
-
path: command.path,
|
|
1586
|
-
requestCount: requests.length,
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
async function handleStateSave(command, browser) {
|
|
1590
|
-
await browser.saveStorageState(command.path);
|
|
1591
|
-
return successResponse(command.id, { path: command.path });
|
|
1592
|
-
}
|
|
1593
|
-
async function handleStateLoad(command, browser) {
|
|
1594
|
-
// Storage state is loaded at context creation
|
|
1595
|
-
return successResponse(command.id, {
|
|
1596
|
-
note: 'Storage state must be loaded at browser launch. Use --state flag.',
|
|
1597
|
-
path: command.path,
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
async function handleConsole(command, browser) {
|
|
1601
|
-
if (command.clear) {
|
|
1602
|
-
browser.clearConsoleMessages();
|
|
1603
|
-
return successResponse(command.id, { cleared: true });
|
|
1604
|
-
}
|
|
1605
|
-
const messages = browser.getConsoleMessages();
|
|
1606
|
-
return successResponse(command.id, { messages });
|
|
1607
|
-
}
|
|
1608
|
-
async function handleErrors(command, browser) {
|
|
1609
|
-
if (command.clear) {
|
|
1610
|
-
browser.clearPageErrors();
|
|
1611
|
-
return successResponse(command.id, { cleared: true });
|
|
1612
|
-
}
|
|
1613
|
-
const errors = browser.getPageErrors();
|
|
1614
|
-
return successResponse(command.id, { errors });
|
|
1615
|
-
}
|
|
1616
|
-
async function handleKeyboard(command, browser) {
|
|
1617
|
-
const page = browser.getPage();
|
|
1618
|
-
await page.keyboard.press(command.keys);
|
|
1619
|
-
return successResponse(command.id, { pressed: command.keys });
|
|
1620
|
-
}
|
|
1621
|
-
async function handleWheel(command, browser) {
|
|
1622
|
-
const page = browser.getPage();
|
|
1623
|
-
if (command.selector) {
|
|
1624
|
-
const element = page.locator(command.selector);
|
|
1625
|
-
await element.hover();
|
|
1626
|
-
}
|
|
1627
|
-
await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
|
|
1628
|
-
return successResponse(command.id, { scrolled: true });
|
|
1629
|
-
}
|
|
1630
|
-
async function handleTap(command, browser) {
|
|
1631
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1632
|
-
await locator.tap();
|
|
1633
|
-
return successResponse(command.id, { tapped: true });
|
|
1634
|
-
}
|
|
1635
|
-
async function handleClipboard(command, browser) {
|
|
1636
|
-
const page = browser.getPage();
|
|
1637
|
-
switch (command.operation) {
|
|
1638
|
-
case 'copy':
|
|
1639
|
-
await page.keyboard.press('Control+c');
|
|
1640
|
-
return successResponse(command.id, { copied: true });
|
|
1641
|
-
case 'paste':
|
|
1642
|
-
await page.keyboard.press('Control+v');
|
|
1643
|
-
return successResponse(command.id, { pasted: true });
|
|
1644
|
-
case 'read':
|
|
1645
|
-
const text = await page.evaluate('navigator.clipboard.readText()');
|
|
1646
|
-
return successResponse(command.id, { text });
|
|
1647
|
-
default:
|
|
1648
|
-
return errorResponse(command.id, 'Unknown clipboard operation');
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
async function handleHighlight(command, browser) {
|
|
1652
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1653
|
-
await locator.highlight();
|
|
1654
|
-
return successResponse(command.id, { highlighted: true });
|
|
1655
|
-
}
|
|
1656
|
-
async function handleClear(command, browser) {
|
|
1657
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1658
|
-
await locator.clear();
|
|
1659
|
-
return successResponse(command.id, { cleared: true });
|
|
1660
|
-
}
|
|
1661
|
-
async function handleSelectAll(command, browser) {
|
|
1662
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1663
|
-
await locator.selectText();
|
|
1664
|
-
return successResponse(command.id, { selected: true });
|
|
1665
|
-
}
|
|
1666
|
-
async function handleInnerText(command, browser) {
|
|
1667
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1668
|
-
const text = await locator.innerText();
|
|
1669
|
-
return successResponse(command.id, { text });
|
|
1670
|
-
}
|
|
1671
|
-
async function handleInnerHtml(command, browser) {
|
|
1672
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1673
|
-
const html = await locator.innerHTML();
|
|
1674
|
-
return successResponse(command.id, { html });
|
|
1675
|
-
}
|
|
1676
|
-
async function handleInputValue(command, browser) {
|
|
1677
|
-
const locator = browser.getLocator(command.selector, command.inFrame);
|
|
1678
|
-
const value = await locator.inputValue();
|
|
1679
|
-
return successResponse(command.id, { value });
|
|
1680
|
-
}
|
|
1681
|
-
async function handleSetValue(command, browser) {
|
|
1682
|
-
const page = browser.getPage();
|
|
1683
|
-
await page.locator(command.selector).fill(command.value);
|
|
1684
|
-
return successResponse(command.id, { set: true });
|
|
1685
|
-
}
|
|
1686
|
-
async function handleDispatch(command, browser) {
|
|
1687
|
-
const page = browser.getPage();
|
|
1688
|
-
await page.locator(command.selector).dispatchEvent(command.event, command.eventInit);
|
|
1689
|
-
return successResponse(command.id, { dispatched: command.event });
|
|
1690
|
-
}
|
|
1691
|
-
async function handleEvalHandle(command, browser) {
|
|
1692
|
-
const page = browser.getPage();
|
|
1693
|
-
const handle = await page.evaluateHandle(command.script);
|
|
1694
|
-
const result = await handle.jsonValue().catch(() => 'Handle (non-serializable)');
|
|
1695
|
-
return successResponse(command.id, { result });
|
|
1696
|
-
}
|
|
1697
|
-
async function handleExpose(command, browser) {
|
|
1698
|
-
const page = browser.getPage();
|
|
1699
|
-
await page.exposeFunction(command.name, () => {
|
|
1700
|
-
// Exposed function - can be extended
|
|
1701
|
-
return `Function ${command.name} called`;
|
|
1702
|
-
});
|
|
1703
|
-
return successResponse(command.id, { exposed: command.name });
|
|
1704
|
-
}
|
|
1705
|
-
async function handleAddScript(command, browser) {
|
|
1706
|
-
const page = browser.getPage();
|
|
1707
|
-
if (command.content) {
|
|
1708
|
-
await page.addScriptTag({ content: command.content });
|
|
1709
|
-
}
|
|
1710
|
-
else if (command.url) {
|
|
1711
|
-
await page.addScriptTag({ url: command.url });
|
|
1712
|
-
}
|
|
1713
|
-
return successResponse(command.id, { added: true });
|
|
1714
|
-
}
|
|
1715
|
-
async function handleAddStyle(command, browser) {
|
|
1716
|
-
const page = browser.getPage();
|
|
1717
|
-
if (command.content) {
|
|
1718
|
-
await page.addStyleTag({ content: command.content });
|
|
1719
|
-
}
|
|
1720
|
-
else if (command.url) {
|
|
1721
|
-
await page.addStyleTag({ url: command.url });
|
|
1722
|
-
}
|
|
1723
|
-
return successResponse(command.id, { added: true });
|
|
1724
|
-
}
|
|
1725
|
-
async function handleEmulateMedia(command, browser) {
|
|
1726
|
-
const page = browser.getPage();
|
|
1727
|
-
await page.emulateMedia({
|
|
1728
|
-
media: command.media,
|
|
1729
|
-
colorScheme: command.colorScheme,
|
|
1730
|
-
reducedMotion: command.reducedMotion,
|
|
1731
|
-
forcedColors: command.forcedColors,
|
|
1732
|
-
});
|
|
1733
|
-
return successResponse(command.id, { emulated: true });
|
|
1734
|
-
}
|
|
1735
|
-
async function handleOffline(command, browser) {
|
|
1736
|
-
await browser.setOffline(command.offline);
|
|
1737
|
-
return successResponse(command.id, { offline: command.offline });
|
|
1738
|
-
}
|
|
1739
|
-
async function handleHeaders(command, browser) {
|
|
1740
|
-
await browser.setExtraHeaders(command.headers);
|
|
1741
|
-
return successResponse(command.id, { set: true });
|
|
1742
|
-
}
|
|
1743
|
-
async function handlePause(command, browser) {
|
|
1744
|
-
const page = browser.getPage();
|
|
1745
|
-
await page.pause();
|
|
1746
|
-
return successResponse(command.id, { paused: true });
|
|
1747
|
-
}
|
|
1748
|
-
async function handleGetByAltText(command, browser) {
|
|
1749
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1750
|
-
const locator = frame.getByAltText(command.text, { exact: command.exact });
|
|
1751
|
-
switch (command.subaction) {
|
|
1752
|
-
case 'click':
|
|
1753
|
-
await locator.click();
|
|
1754
|
-
return successResponse(command.id, { clicked: true });
|
|
1755
|
-
case 'hover':
|
|
1756
|
-
await locator.hover();
|
|
1757
|
-
return successResponse(command.id, { hovered: true });
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
async function handleGetByTitle(command, browser) {
|
|
1761
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1762
|
-
const locator = frame.getByTitle(command.text, { exact: command.exact });
|
|
1763
|
-
switch (command.subaction) {
|
|
1764
|
-
case 'click':
|
|
1765
|
-
await locator.click();
|
|
1766
|
-
return successResponse(command.id, { clicked: true });
|
|
1767
|
-
case 'hover':
|
|
1768
|
-
await locator.hover();
|
|
1769
|
-
return successResponse(command.id, { hovered: true });
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
async function handleGetByTestId(command, browser) {
|
|
1773
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1774
|
-
const locator = frame.getByTestId(command.testId);
|
|
1775
|
-
switch (command.subaction) {
|
|
1776
|
-
case 'click':
|
|
1777
|
-
await locator.click();
|
|
1778
|
-
return successResponse(command.id, { clicked: true });
|
|
1779
|
-
case 'fill':
|
|
1780
|
-
await locator.fill(command.value ?? '');
|
|
1781
|
-
return successResponse(command.id, { filled: true });
|
|
1782
|
-
case 'check':
|
|
1783
|
-
await locator.check();
|
|
1784
|
-
return successResponse(command.id, { checked: true });
|
|
1785
|
-
case 'hover':
|
|
1786
|
-
await locator.hover();
|
|
1787
|
-
return successResponse(command.id, { hovered: true });
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
async function handleNth(command, browser) {
|
|
1791
|
-
const refLocator = browser.getLocatorFromRef(command.selector, command.inFrame);
|
|
1792
|
-
let locator;
|
|
1793
|
-
if (refLocator) {
|
|
1794
|
-
locator = command.index === -1 ? refLocator.last() : refLocator.nth(command.index);
|
|
1795
|
-
}
|
|
1796
|
-
else {
|
|
1797
|
-
const frame = browser.getFrame(command.inFrame);
|
|
1798
|
-
const base = frame.locator(command.selector);
|
|
1799
|
-
locator = command.index === -1 ? base.last() : base.nth(command.index);
|
|
1800
|
-
}
|
|
1801
|
-
switch (command.subaction) {
|
|
1802
|
-
case 'click':
|
|
1803
|
-
await locator.click();
|
|
1804
|
-
return successResponse(command.id, { clicked: true });
|
|
1805
|
-
case 'fill':
|
|
1806
|
-
await locator.fill(command.value ?? '');
|
|
1807
|
-
return successResponse(command.id, { filled: true });
|
|
1808
|
-
case 'check':
|
|
1809
|
-
await locator.check();
|
|
1810
|
-
return successResponse(command.id, { checked: true });
|
|
1811
|
-
case 'hover':
|
|
1812
|
-
await locator.hover();
|
|
1813
|
-
return successResponse(command.id, { hovered: true });
|
|
1814
|
-
case 'text':
|
|
1815
|
-
const text = await locator.textContent();
|
|
1816
|
-
return successResponse(command.id, { text });
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
async function handleWaitForUrl(command, browser) {
|
|
1820
|
-
const page = browser.getPage();
|
|
1821
|
-
await page.waitForURL(command.url, { timeout: command.timeout });
|
|
1822
|
-
return successResponse(command.id, { url: page.url() });
|
|
1823
|
-
}
|
|
1824
|
-
async function handleWaitForLoadState(command, browser) {
|
|
1825
|
-
const page = browser.getPage();
|
|
1826
|
-
await page.waitForLoadState(command.state, { timeout: command.timeout });
|
|
1827
|
-
return successResponse(command.id, { state: command.state });
|
|
1828
|
-
}
|
|
1829
|
-
async function handleSetContent(command, browser) {
|
|
1830
|
-
const page = browser.getPage();
|
|
1831
|
-
await page.setContent(command.html);
|
|
1832
|
-
return successResponse(command.id, { set: true });
|
|
1833
|
-
}
|
|
1834
|
-
async function handleTimezone(command, browser) {
|
|
1835
|
-
// Timezone must be set at context level before navigation
|
|
1836
|
-
// This is a limitation - it sets for the current context
|
|
1837
|
-
const page = browser.getPage();
|
|
1838
|
-
await page.context().setGeolocation({ latitude: 0, longitude: 0 }); // Trigger context awareness
|
|
1839
|
-
return successResponse(command.id, {
|
|
1840
|
-
note: 'Timezone must be set at browser launch. Use --timezone flag.',
|
|
1841
|
-
timezone: command.timezone,
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
async function handleLocale(command, browser) {
|
|
1845
|
-
// Locale must be set at context creation
|
|
1846
|
-
return successResponse(command.id, {
|
|
1847
|
-
note: 'Locale must be set at browser launch. Use --locale flag.',
|
|
1848
|
-
locale: command.locale,
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
async function handleCredentials(command, browser) {
|
|
1852
|
-
const context = browser.getPage().context();
|
|
1853
|
-
await context.setHTTPCredentials({
|
|
1854
|
-
username: command.username,
|
|
1855
|
-
password: command.password,
|
|
1856
|
-
});
|
|
1857
|
-
return successResponse(command.id, { set: true });
|
|
1858
|
-
}
|
|
1859
|
-
async function handleMouseMove(command, browser) {
|
|
1860
|
-
const page = browser.getPage();
|
|
1861
|
-
await page.mouse.move(command.x, command.y);
|
|
1862
|
-
return successResponse(command.id, { moved: true, x: command.x, y: command.y });
|
|
1863
|
-
}
|
|
1864
|
-
async function handleMouseDown(command, browser) {
|
|
1865
|
-
const page = browser.getPage();
|
|
1866
|
-
await page.mouse.down({ button: command.button ?? 'left' });
|
|
1867
|
-
return successResponse(command.id, { down: true });
|
|
1868
|
-
}
|
|
1869
|
-
async function handleMouseUp(command, browser) {
|
|
1870
|
-
const page = browser.getPage();
|
|
1871
|
-
await page.mouse.up({ button: command.button ?? 'left' });
|
|
1872
|
-
return successResponse(command.id, { up: true });
|
|
1873
|
-
}
|
|
1874
|
-
async function handleWander(command, browser) {
|
|
1875
|
-
const page = browser.getPage();
|
|
1876
|
-
const viewport = page.viewportSize() ?? { width: 1280, height: 800 };
|
|
1877
|
-
const config = command.human ?? { enabled: true, pathType: 'bezier' };
|
|
1878
|
-
await humanWander(page, config, {
|
|
1879
|
-
duration: command.duration ?? 2000,
|
|
1880
|
-
area: viewport,
|
|
1881
|
-
});
|
|
1882
|
-
return successResponse(command.id, { wandered: true, duration: command.duration ?? 2000 });
|
|
1883
|
-
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Parse trajectory data string into array of points
|
|
1886
|
-
* Format: "x:y:delay;x:y:delay;..."
|
|
1887
|
-
*/
|
|
1888
|
-
function parseTrajectoryData(data) {
|
|
1889
|
-
return data.split(';').map((segment) => {
|
|
1890
|
-
const parts = segment.split(':').map(Number);
|
|
1891
|
-
return { x: parts[0] || 0, y: parts[1] || 0, delay: parts[2] || 0 };
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
async function handleMouseTrajectory(command, browser) {
|
|
1895
|
-
const page = browser.getPage();
|
|
1896
|
-
const points = parseTrajectoryData(command.data);
|
|
1897
|
-
const config = command.human ?? { enabled: true, pathType: 'bezier' };
|
|
1898
|
-
for (let i = 0; i < points.length; i++) {
|
|
1899
|
-
const { x, y, delay } = points[i];
|
|
1900
|
-
// Wait for the specified delay (except first point)
|
|
1901
|
-
if (delay > 0) {
|
|
1902
|
-
await page.waitForTimeout(delay);
|
|
1903
|
-
}
|
|
1904
|
-
// Move with human-like animation if enabled
|
|
1905
|
-
if (config.enabled) {
|
|
1906
|
-
await humanMoveTo(page, { x, y }, config);
|
|
1907
|
-
}
|
|
1908
|
-
else {
|
|
1909
|
-
await page.mouse.move(x, y);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
return successResponse(command.id, { moved: true, points: points.length });
|
|
1913
|
-
}
|
|
1914
|
-
async function handleBringToFront(command, browser) {
|
|
1915
|
-
const page = browser.getPage();
|
|
1916
|
-
await page.bringToFront();
|
|
1917
|
-
return successResponse(command.id, { focused: true });
|
|
1918
|
-
}
|
|
1919
|
-
async function handleWaitForFunction(command, browser) {
|
|
1920
|
-
const page = browser.getPage();
|
|
1921
|
-
await page.waitForFunction(command.expression, { timeout: command.timeout });
|
|
1922
|
-
return successResponse(command.id, { waited: true });
|
|
1923
|
-
}
|
|
1924
|
-
async function handleScrollIntoView(command, browser) {
|
|
1925
|
-
const page = browser.getPage();
|
|
1926
|
-
await page.locator(command.selector).scrollIntoViewIfNeeded();
|
|
1927
|
-
return successResponse(command.id, { scrolled: true });
|
|
1928
|
-
}
|
|
1929
|
-
export async function handleAddInitScript(command, browser) {
|
|
1930
|
-
const page = browser.getPage();
|
|
1931
|
-
const context = page.context();
|
|
1932
|
-
await context.addInitScript(command.script);
|
|
1933
|
-
const tips = [];
|
|
1934
|
-
try {
|
|
1935
|
-
await page.evaluate(command.script);
|
|
1936
|
-
}
|
|
1937
|
-
catch (e) {
|
|
1938
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1939
|
-
tips.push(`Init script error on current page: ${msg}. Script will work on next navigation.`);
|
|
1940
|
-
}
|
|
1941
|
-
return successResponse(command.id, { added: true }, tips.length ? tips : undefined);
|
|
1942
|
-
}
|
|
1943
|
-
async function handleKeyDown(command, browser) {
|
|
1944
|
-
const page = browser.getPage();
|
|
1945
|
-
await page.keyboard.down(command.key);
|
|
1946
|
-
return successResponse(command.id, { down: true, key: command.key });
|
|
1947
|
-
}
|
|
1948
|
-
async function handleKeyUp(command, browser) {
|
|
1949
|
-
const page = browser.getPage();
|
|
1950
|
-
await page.keyboard.up(command.key);
|
|
1951
|
-
return successResponse(command.id, { up: true, key: command.key });
|
|
1952
|
-
}
|
|
1953
|
-
async function handleInsertText(command, browser) {
|
|
1954
|
-
const page = browser.getPage();
|
|
1955
|
-
await page.keyboard.insertText(command.text);
|
|
1956
|
-
return successResponse(command.id, { inserted: true });
|
|
1957
|
-
}
|
|
1958
|
-
async function handleMultiSelect(command, browser) {
|
|
1959
|
-
const page = browser.getPage();
|
|
1960
|
-
const selected = await page.locator(command.selector).selectOption(command.values);
|
|
1961
|
-
return successResponse(command.id, { selected });
|
|
1962
|
-
}
|
|
1963
|
-
async function handleWaitForDownload(command, browser) {
|
|
1964
|
-
const page = browser.getPage();
|
|
1965
|
-
const download = await page.waitForEvent('download', { timeout: command.timeout });
|
|
1966
|
-
let filePath;
|
|
1967
|
-
if (command.path) {
|
|
1968
|
-
filePath = command.path;
|
|
1969
|
-
await download.saveAs(filePath);
|
|
1970
|
-
}
|
|
1971
|
-
else {
|
|
1972
|
-
filePath = (await download.path()) || download.suggestedFilename();
|
|
1973
|
-
}
|
|
1974
|
-
return successResponse(command.id, {
|
|
1975
|
-
path: filePath,
|
|
1976
|
-
filename: download.suggestedFilename(),
|
|
1977
|
-
url: download.url(),
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
|
-
async function handleResponseBody(command, browser) {
|
|
1981
|
-
const page = browser.getPage();
|
|
1982
|
-
const response = await page.waitForResponse((resp) => resp.url().includes(command.url), {
|
|
1983
|
-
timeout: command.timeout,
|
|
1984
|
-
});
|
|
1985
|
-
const body = await response.text();
|
|
1986
|
-
let parsed = body;
|
|
1987
|
-
try {
|
|
1988
|
-
parsed = JSON.parse(body);
|
|
1989
|
-
}
|
|
1990
|
-
catch {
|
|
1991
|
-
// Keep as string if not JSON
|
|
1992
|
-
}
|
|
1993
|
-
return successResponse(command.id, {
|
|
1994
|
-
url: response.url(),
|
|
1995
|
-
status: response.status(),
|
|
1996
|
-
body: parsed,
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
// Screencast and input injection handlers
|
|
2000
|
-
async function handleScreencastStart(command, browser) {
|
|
2001
|
-
if (!screencastFrameCallback) {
|
|
2002
|
-
throw new Error('Screencast frame callback not set. Start the streaming server first.');
|
|
2003
|
-
}
|
|
2004
|
-
await browser.startScreencast(screencastFrameCallback, {
|
|
2005
|
-
format: command.format,
|
|
2006
|
-
quality: command.quality,
|
|
2007
|
-
maxWidth: command.maxWidth,
|
|
2008
|
-
maxHeight: command.maxHeight,
|
|
2009
|
-
everyNthFrame: command.everyNthFrame,
|
|
2010
|
-
});
|
|
2011
|
-
return successResponse(command.id, {
|
|
2012
|
-
started: true,
|
|
2013
|
-
format: command.format ?? 'jpeg',
|
|
2014
|
-
quality: command.quality ?? 80,
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
2017
|
-
async function handleScreencastStop(command, browser) {
|
|
2018
|
-
await browser.stopScreencast();
|
|
2019
|
-
return successResponse(command.id, { stopped: true });
|
|
2020
|
-
}
|
|
2021
|
-
async function handleInputMouse(command, browser) {
|
|
2022
|
-
await browser.injectMouseEvent({
|
|
2023
|
-
type: command.type,
|
|
2024
|
-
x: command.x,
|
|
2025
|
-
y: command.y,
|
|
2026
|
-
button: command.button,
|
|
2027
|
-
clickCount: command.clickCount,
|
|
2028
|
-
deltaX: command.deltaX,
|
|
2029
|
-
deltaY: command.deltaY,
|
|
2030
|
-
modifiers: command.modifiers,
|
|
2031
|
-
});
|
|
2032
|
-
return successResponse(command.id, { injected: true });
|
|
2033
|
-
}
|
|
2034
|
-
async function handleInputKeyboard(command, browser) {
|
|
2035
|
-
await browser.injectKeyboardEvent({
|
|
2036
|
-
type: command.type,
|
|
2037
|
-
key: command.key,
|
|
2038
|
-
code: command.code,
|
|
2039
|
-
text: command.text,
|
|
2040
|
-
modifiers: command.modifiers,
|
|
2041
|
-
});
|
|
2042
|
-
return successResponse(command.id, { injected: true });
|
|
2043
|
-
}
|
|
2044
|
-
async function handleInputTouch(command, browser) {
|
|
2045
|
-
await browser.injectTouchEvent({
|
|
2046
|
-
type: command.type,
|
|
2047
|
-
touchPoints: command.touchPoints,
|
|
2048
|
-
modifiers: command.modifiers,
|
|
2049
|
-
});
|
|
2050
|
-
return successResponse(command.id, { injected: true });
|
|
2051
|
-
}
|
|
2052
|
-
// Recording handlers (Playwright native video recording)
|
|
2053
|
-
async function handleRecordingStart(command, browser) {
|
|
2054
|
-
await browser.startRecording(command.path, command.url);
|
|
2055
|
-
return successResponse(command.id, {
|
|
2056
|
-
started: true,
|
|
2057
|
-
path: command.path,
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
async function handleRecordingStop(command, browser) {
|
|
2061
|
-
const result = await browser.stopRecording();
|
|
2062
|
-
return successResponse(command.id, result);
|
|
2063
|
-
}
|
|
2064
|
-
async function handleRecordingRestart(command, browser) {
|
|
2065
|
-
const result = await browser.restartRecording(command.path, command.url);
|
|
2066
|
-
return successResponse(command.id, {
|
|
2067
|
-
started: true,
|
|
2068
|
-
path: command.path,
|
|
2069
|
-
previousPath: result.previousPath,
|
|
2070
|
-
stopped: result.stopped,
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
2073
|
-
async function handleRecorderStart(command, browser) {
|
|
2074
|
-
const result = await browser.startRecorder(command.url, command.hide);
|
|
2075
|
-
return successResponse(command.id, result);
|
|
2076
|
-
}
|
|
2077
|
-
async function handleRecorderStop(command, browser) {
|
|
2078
|
-
const result = await browser.stopRecorder();
|
|
2079
|
-
// 如果没有在录制中,返回提示但仍尝试获取最近的录制文件
|
|
2080
|
-
if (result.wasRecording === false) {
|
|
2081
|
-
// 尝试获取最近的录制文件
|
|
2082
|
-
const recorderDir = path.join(getAppDir(), 'tmp', 'recordings');
|
|
2083
|
-
if (existsSync(recorderDir)) {
|
|
2084
|
-
const files = readdirSync(recorderDir)
|
|
2085
|
-
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
2086
|
-
.map((f) => ({
|
|
2087
|
-
name: f,
|
|
2088
|
-
time: statSync(path.join(recorderDir, f)).mtime.getTime(),
|
|
2089
|
-
}))
|
|
2090
|
-
.sort((a, b) => b.time - a.time);
|
|
2091
|
-
if (files.length > 0) {
|
|
2092
|
-
const recentPath = path.join(recorderDir, files[0].name);
|
|
2093
|
-
return successResponse(command.id, {
|
|
2094
|
-
yaml: '',
|
|
2095
|
-
steps: 0,
|
|
2096
|
-
path: recentPath,
|
|
2097
|
-
note: 'No active recording session. Returning most recent recording file.',
|
|
2098
|
-
});
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
return successResponse(command.id, {
|
|
2102
|
-
yaml: '',
|
|
2103
|
-
steps: 0,
|
|
2104
|
-
note: 'No active recording session. Use "recorder start" to begin recording.',
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
// 确定输出路径
|
|
2108
|
-
let outputPath;
|
|
2109
|
-
let isDefaultPath = false;
|
|
2110
|
-
if (command.output) {
|
|
2111
|
-
outputPath = path.resolve(command.output);
|
|
2112
|
-
}
|
|
2113
|
-
else {
|
|
2114
|
-
// 默认保存到临时目录
|
|
2115
|
-
const recorderDir = path.join(getAppDir(), 'tmp', 'recordings');
|
|
2116
|
-
if (!existsSync(recorderDir)) {
|
|
2117
|
-
mkdirSync(recorderDir, { recursive: true });
|
|
2118
|
-
}
|
|
2119
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2120
|
-
outputPath = path.join(recorderDir, `session-${timestamp}.yaml`);
|
|
2121
|
-
isDefaultPath = true;
|
|
2122
|
-
}
|
|
2123
|
-
// 保存文件
|
|
2124
|
-
writeFileSync(outputPath, result.yaml, 'utf-8');
|
|
2125
|
-
// 返回 YAML 内容和路径
|
|
2126
|
-
return successResponse(command.id, {
|
|
2127
|
-
yaml: result.yaml,
|
|
2128
|
-
steps: result.steps,
|
|
2129
|
-
path: outputPath,
|
|
2130
|
-
tip: isDefaultPath ? `Full YAML saved to: ${outputPath}` : undefined,
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
async function handleRecorderStatus(command, browser) {
|
|
2134
|
-
const result = browser.getRecorderStatus();
|
|
2135
|
-
return successResponse(command.id, result);
|
|
2136
|
-
}
|
|
2137
|
-
/**
|
|
2138
|
-
* Parse and execute CLI commands from YAML file
|
|
2139
|
-
*/
|
|
2140
|
-
async function handleRecorderReplay(command, browser) {
|
|
2141
|
-
const fs = await import('node:fs');
|
|
2142
|
-
const path = await import('node:path');
|
|
2143
|
-
let yamlPath = command.path;
|
|
2144
|
-
if (!yamlPath) {
|
|
2145
|
-
const recorderDir = path.join(getAppDir(), 'tmp', 'recordings');
|
|
2146
|
-
if (!fs.existsSync(recorderDir)) {
|
|
2147
|
-
return errorResponse(command.id, 'No recordings found. Please record first.');
|
|
2148
|
-
}
|
|
2149
|
-
const files = fs
|
|
2150
|
-
.readdirSync(recorderDir)
|
|
2151
|
-
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
2152
|
-
.map((f) => ({
|
|
2153
|
-
name: f,
|
|
2154
|
-
time: fs.statSync(path.join(recorderDir, f)).mtime.getTime(),
|
|
2155
|
-
}))
|
|
2156
|
-
.sort((a, b) => b.time - a.time);
|
|
2157
|
-
if (files.length === 0) {
|
|
2158
|
-
return errorResponse(command.id, 'No recordings found. Please record first.');
|
|
2159
|
-
}
|
|
2160
|
-
yamlPath = path.join(recorderDir, files[0].name);
|
|
2161
|
-
}
|
|
2162
|
-
if (!fs.existsSync(yamlPath)) {
|
|
2163
|
-
return errorResponse(command.id, `Recording file not found: ${yamlPath}`);
|
|
2164
|
-
}
|
|
2165
|
-
const yamlContent = fs.readFileSync(yamlPath, 'utf-8');
|
|
2166
|
-
const cliCommands = [];
|
|
2167
|
-
// Strategy 1: Parse structured steps and generate CLI commands
|
|
2168
|
-
const stepRegex = /^\s+-\s+(?:id:\s*.+)/;
|
|
2169
|
-
const lines = yamlContent.split('\n');
|
|
2170
|
-
let inSteps = false;
|
|
2171
|
-
const parsedSteps = {};
|
|
2172
|
-
for (const line of lines) {
|
|
2173
|
-
if (/^steps:/.test(line.trim())) {
|
|
2174
|
-
inSteps = true;
|
|
2175
|
-
continue;
|
|
2176
|
-
}
|
|
2177
|
-
if (inSteps && /^-\s+id:/.test(line.trim())) {
|
|
2178
|
-
const idMatch = line.match(/id:\s*(.+)/);
|
|
2179
|
-
if (idMatch)
|
|
2180
|
-
parsedSteps.currentId = idMatch[1].trim();
|
|
2181
|
-
}
|
|
2182
|
-
if (inSteps && /^\s+action:\s*(.+)/.test(line)) {
|
|
2183
|
-
// End of steps section when we hit a non-step line
|
|
2184
|
-
}
|
|
2185
|
-
if (inSteps && !/^\s/.test(line) && !/^$/.test(line) && !/^steps:/.test(line.trim())) {
|
|
2186
|
-
inSteps = false;
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
// Strategy 2: Fall back to CLI Commands comment section
|
|
2190
|
-
let inCliSection = false;
|
|
2191
|
-
for (const line of lines) {
|
|
2192
|
-
if (line.includes('# CLI Commands')) {
|
|
2193
|
-
inCliSection = true;
|
|
2194
|
-
continue;
|
|
2195
|
-
}
|
|
2196
|
-
if (inCliSection &&
|
|
2197
|
-
(line.startsWith('agent-browser ') || line.startsWith('AGENT_BROWSER_HUMAN='))) {
|
|
2198
|
-
cliCommands.push(line.trim());
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
// Strategy 3: If no CLI section, generate from structured steps using browser's method
|
|
2202
|
-
if (cliCommands.length === 0) {
|
|
2203
|
-
return errorResponse(command.id, 'No CLI commands found in recording. Please re-record with the new version.');
|
|
2204
|
-
}
|
|
2205
|
-
// Filter out env-only lines (keep them for env setup but not as commands)
|
|
2206
|
-
const envLines = cliCommands.filter((l) => l.startsWith('AGENT_BROWSER_'));
|
|
2207
|
-
const cmdLines = cliCommands.filter((l) => l.startsWith('agent-browser '));
|
|
2208
|
-
// Set env vars from recording (skip AGENT_BROWSER_HUMAN to avoid
|
|
2209
|
-
// human-mode coordinate issues during replay where elements may be
|
|
2210
|
-
// scrolled off-screen)
|
|
2211
|
-
const originalEnv = {};
|
|
2212
|
-
for (const envLine of envLines) {
|
|
2213
|
-
const eqIdx = envLine.indexOf('=');
|
|
2214
|
-
if (eqIdx > 0) {
|
|
2215
|
-
const key = envLine.substring(0, eqIdx);
|
|
2216
|
-
if (key === 'AGENT_BROWSER_HUMAN')
|
|
2217
|
-
continue;
|
|
2218
|
-
let value = envLine.substring(eqIdx + 1);
|
|
2219
|
-
const spaceIdx = value.indexOf(' ');
|
|
2220
|
-
if (spaceIdx > 0) {
|
|
2221
|
-
value = value.substring(0, spaceIdx);
|
|
2222
|
-
}
|
|
2223
|
-
originalEnv[key] = process.env[key];
|
|
2224
|
-
process.env[key] = value;
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
function parseCommandLine(line) {
|
|
2228
|
-
const parts = [];
|
|
2229
|
-
let current = '';
|
|
2230
|
-
let inQuotes = false;
|
|
2231
|
-
let quoteChar = '';
|
|
2232
|
-
for (let i = 0; i < line.length; i++) {
|
|
2233
|
-
const char = line[i];
|
|
2234
|
-
if ((char === '"' || char === "'") && !inQuotes) {
|
|
2235
|
-
inQuotes = true;
|
|
2236
|
-
quoteChar = char;
|
|
2237
|
-
}
|
|
2238
|
-
else if (char === quoteChar && inQuotes) {
|
|
2239
|
-
inQuotes = false;
|
|
2240
|
-
quoteChar = '';
|
|
2241
|
-
}
|
|
2242
|
-
else if (char === ' ' && !inQuotes) {
|
|
2243
|
-
if (current) {
|
|
2244
|
-
parts.push(current);
|
|
2245
|
-
current = '';
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
else {
|
|
2249
|
-
current += char;
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
if (current) {
|
|
2253
|
-
parts.push(current);
|
|
2254
|
-
}
|
|
2255
|
-
return parts;
|
|
2256
|
-
}
|
|
2257
|
-
// Get current session for passthrough
|
|
2258
|
-
const currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
|
|
2259
|
-
const results = [];
|
|
2260
|
-
for (const cmdLine of cmdLines) {
|
|
2261
|
-
try {
|
|
2262
|
-
let parts = parseCommandLine(cmdLine);
|
|
2263
|
-
if (parts.length > 0 && parts[0] === 'agent-browser') {
|
|
2264
|
-
parts = parts.slice(1);
|
|
2265
|
-
}
|
|
2266
|
-
if (parts.length === 0) {
|
|
2267
|
-
results.push({ command: cmdLine, success: true });
|
|
2268
|
-
continue;
|
|
2269
|
-
}
|
|
2270
|
-
const { parseCommand } = await import('./cli/commands.js');
|
|
2271
|
-
const { parseFlags } = await import('./cli/flags.js');
|
|
2272
|
-
const flags = parseFlags([]);
|
|
2273
|
-
if (currentSession !== 'default') {
|
|
2274
|
-
flags.session = currentSession;
|
|
2275
|
-
}
|
|
2276
|
-
const parsedCmd = parseCommand(parts, flags);
|
|
2277
|
-
const wasRecording = browser.isRecordingSession();
|
|
2278
|
-
if (wasRecording) {
|
|
2279
|
-
browser.pauseRecording();
|
|
2280
|
-
}
|
|
2281
|
-
const COMMAND_TIMEOUT_MS = 5000;
|
|
2282
|
-
const result = (await Promise.race([
|
|
2283
|
-
executeCommand(parsedCmd, browser),
|
|
2284
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Command timed out after ${COMMAND_TIMEOUT_MS}ms`)), COMMAND_TIMEOUT_MS)),
|
|
2285
|
-
]));
|
|
2286
|
-
if (wasRecording) {
|
|
2287
|
-
browser.resumeRecording();
|
|
2288
|
-
}
|
|
2289
|
-
results.push({ command: cmdLine, success: result.success });
|
|
2290
|
-
}
|
|
2291
|
-
catch (e) {
|
|
2292
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2293
|
-
results.push({ command: cmdLine, success: false, error: errorMessage });
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
// Restore env vars
|
|
2297
|
-
for (const [key, value] of Object.entries(originalEnv)) {
|
|
2298
|
-
if (value === undefined) {
|
|
2299
|
-
delete process.env[key];
|
|
2300
|
-
}
|
|
2301
|
-
else {
|
|
2302
|
-
process.env[key] = value;
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
const successCount = results.filter((r) => r.success).length;
|
|
2306
|
-
const failCount = results.filter((r) => !r.success).length;
|
|
2307
|
-
return successResponse(command.id, {
|
|
2308
|
-
replayed: true,
|
|
2309
|
-
file: yamlPath,
|
|
2310
|
-
totalCommands: cmdLines.length,
|
|
2311
|
-
successCount,
|
|
2312
|
-
failCount,
|
|
2313
|
-
results: results.slice(0, 20),
|
|
2314
|
-
});
|
|
2315
|
-
}
|
|
2316
|
-
async function handleViewer(command, _browser) {
|
|
2317
|
-
const instanceId = getInstanceId();
|
|
2318
|
-
return successResponse(command.id, {
|
|
2319
|
-
url: getViewerUrl(instanceId),
|
|
2320
|
-
wsUrl: getViewerWsUrl(instanceId),
|
|
2321
|
-
streamPort: getViewerPort(),
|
|
2322
|
-
});
|
|
2323
|
-
}
|
|
2324
|
-
async function handleAsk(command, _browser) {
|
|
2325
|
-
const session = getSession();
|
|
2326
|
-
const bridge = new MessageBridge(getMessageBridgeUrl());
|
|
2327
|
-
try {
|
|
2328
|
-
const answer = await bridge.ask(command.question, session);
|
|
2329
|
-
return successResponse(command.id, { answer });
|
|
2330
|
-
}
|
|
2331
|
-
catch (error) {
|
|
2332
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2333
|
-
return errorResponse(command.id, `Failed to ask question: ${message}`);
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
function handleConfig(command) {
|
|
2337
|
-
const humanConfig = getHumanConfigFromEnv();
|
|
2338
|
-
const rcConfig = loadConfig();
|
|
2339
|
-
const config = {
|
|
2340
|
-
session: process.env.AGENT_BROWSER_SESSION || 'default',
|
|
2341
|
-
executablePath: getExecutablePath() || null,
|
|
2342
|
-
extensions: process.env.AGENT_BROWSER_EXTENSIONS || null,
|
|
2343
|
-
profile: process.env.AGENT_BROWSER_PROFILE || null,
|
|
2344
|
-
state: process.env.AGENT_BROWSER_STATE || null,
|
|
2345
|
-
proxy: process.env.AGENT_BROWSER_PROXY || null,
|
|
2346
|
-
proxyBypass: process.env.AGENT_BROWSER_PROXY_BYPASS || null,
|
|
2347
|
-
args: process.env.AGENT_BROWSER_ARGS || null,
|
|
2348
|
-
userAgent: process.env.AGENT_BROWSER_USER_AGENT || null,
|
|
2349
|
-
provider: process.env.AGENT_BROWSER_PROVIDER || null,
|
|
2350
|
-
allowFileAccess: process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1',
|
|
2351
|
-
streamPort: getViewerPort(),
|
|
2352
|
-
headed: process.env.AGENT_BROWSER_HEADED === '1',
|
|
2353
|
-
human: humanConfig,
|
|
2354
|
-
};
|
|
2355
|
-
if (command.json) {
|
|
2356
|
-
return successResponse(command.id, { config, rc: rcConfig });
|
|
2357
|
-
}
|
|
2358
|
-
const viewerHost = getEffectiveValue('viewer.host');
|
|
2359
|
-
const bridgeUrl = getEffectiveValue('messageBridge.url');
|
|
2360
|
-
const msgProxy = getEffectiveValue('messageProxy.url');
|
|
2361
|
-
// Format human-readable output
|
|
2362
|
-
const lines = [
|
|
2363
|
-
'Agent Browser Configuration',
|
|
2364
|
-
'===========================',
|
|
2365
|
-
'',
|
|
2366
|
-
'Session & Browser:',
|
|
2367
|
-
` executablePath ${config.executablePath || '(not set)'}`,
|
|
2368
|
-
` AGENT_BROWSER_PROVIDER ${config.provider || '(not set)'}`,
|
|
2369
|
-
` AGENT_BROWSER_HEADED ${config.headed ? 'true' : 'false (default)'}`,
|
|
2370
|
-
'',
|
|
2371
|
-
'Viewer & Stream:',
|
|
2372
|
-
` viewer.host ${viewerHost || '(not set, using http://localhost)'}`,
|
|
2373
|
-
` viewer.port ${config.streamPort}`,
|
|
2374
|
-
'',
|
|
2375
|
-
'Message Bridge (ask command):',
|
|
2376
|
-
` messageBridge.url ${bridgeUrl || '(not set, using default)'}`,
|
|
2377
|
-
` messageProxy.url ${msgProxy || '(not set)'}`,
|
|
2378
|
-
'',
|
|
2379
|
-
'Browser Options:',
|
|
2380
|
-
` AGENT_BROWSER_PROFILE ${config.profile || '(not set)'}`,
|
|
2381
|
-
` AGENT_BROWSER_EXTENSIONS ${config.extensions || '(not set)'}`,
|
|
2382
|
-
` AGENT_BROWSER_ARGS ${config.args || '(not set)'}`,
|
|
2383
|
-
` AGENT_BROWSER_USER_AGENT ${config.userAgent || '(not set)'}`,
|
|
2384
|
-
` AGENT_BROWSER_PROXY ${config.proxy || '(not set)'}`,
|
|
2385
|
-
` AGENT_BROWSER_ALLOW_FILE_ACCESS ${config.allowFileAccess ? 'true' : 'false (default)'}`,
|
|
2386
|
-
'',
|
|
2387
|
-
'Human Mode (runtime):',
|
|
2388
|
-
` AGENT_BROWSER_HUMAN ${humanConfig.enabled ? humanConfig.pathType + ' ✓' : '(disabled)'}`,
|
|
2389
|
-
'',
|
|
2390
|
-
`Persistent config: ~/.agent-browser/config.json`,
|
|
2391
|
-
'Run "agent-browser config set <key> <value>" to persist settings.',
|
|
2392
|
-
'Run "agent-browser config list" to see configurable keys.',
|
|
2393
|
-
];
|
|
2394
|
-
return successResponse(command.id, { config, output: lines.join('\n') });
|
|
2395
|
-
}
|
|
2396
|
-
async function handleHistory(command, browser) {
|
|
2397
|
-
if (command.clear) {
|
|
2398
|
-
browser.clearHistory();
|
|
2399
|
-
return successResponse(command.id, { cleared: true });
|
|
2400
|
-
}
|
|
2401
|
-
const history = browser.getHistory(command.filter);
|
|
2402
|
-
return successResponse(command.id, { history });
|
|
2403
|
-
}
|
|
2404
|
-
async function handleSelectorFor(command, browser) {
|
|
2405
|
-
const store = browser.getSnapshotStore();
|
|
2406
|
-
const colonIndex = command.target.indexOf(':');
|
|
2407
|
-
if (colonIndex === -1) {
|
|
2408
|
-
return errorResponse(command.id, `Invalid target format: "${command.target}". Expected "snapshotId:refOrIndex" (e.g., "snap_3:@e1" or "snap_3:1").`);
|
|
2409
|
-
}
|
|
2410
|
-
const snapshotId = command.target.substring(0, colonIndex);
|
|
2411
|
-
const refOrIndex = command.target.substring(colonIndex + 1);
|
|
2412
|
-
await browser.ensureSelectorsGenerated(snapshotId);
|
|
2413
|
-
const element = store.getElement(snapshotId, refOrIndex);
|
|
2414
|
-
if (!element) {
|
|
2415
|
-
return errorResponse(command.id, `Element not found: "${refOrIndex}" in snapshot "${snapshotId}". Run 'snapshot' to get fresh snapshot data.`);
|
|
2416
|
-
}
|
|
2417
|
-
return successResponse(command.id, {
|
|
2418
|
-
snapshotId,
|
|
2419
|
-
ref: element.ref,
|
|
2420
|
-
index: element.index,
|
|
2421
|
-
role: element.role,
|
|
2422
|
-
name: element.name,
|
|
2423
|
-
cssSelector: element.cssSelector,
|
|
2424
|
-
xpath: element.xpath,
|
|
2425
|
-
});
|
|
2426
|
-
}
|
|
2427
|
-
async function handleSelectorsOf(command, browser) {
|
|
2428
|
-
const store = browser.getSnapshotStore();
|
|
2429
|
-
await browser.ensureSelectorsGenerated(command.target);
|
|
2430
|
-
const elements = store.getElements(command.target);
|
|
2431
|
-
if (!elements) {
|
|
2432
|
-
return errorResponse(command.id, `Snapshot "${command.target}" not found. Run 'snapshot' to create a new snapshot.`);
|
|
2433
|
-
}
|
|
2434
|
-
return successResponse(command.id, {
|
|
2435
|
-
snapshotId: command.target,
|
|
2436
|
-
elements: elements.map((el) => ({
|
|
2437
|
-
ref: el.ref,
|
|
2438
|
-
index: el.index,
|
|
2439
|
-
role: el.role,
|
|
2440
|
-
name: el.name,
|
|
2441
|
-
cssSelector: el.cssSelector,
|
|
2442
|
-
xpath: el.xpath,
|
|
2443
|
-
})),
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
|
-
async function handleValidate(command, browser) {
|
|
2447
|
-
const store = browser.getSnapshotStore();
|
|
2448
|
-
await browser.ensureSelectorsGenerated(command.target);
|
|
2449
|
-
const elements = store.getElements(command.target);
|
|
2450
|
-
if (!elements) {
|
|
2451
|
-
return errorResponse(command.id, `Snapshot "${command.target}" not found. Run 'snapshot' to create a new snapshot.`);
|
|
2452
|
-
}
|
|
2453
|
-
const page = browser.getPage();
|
|
2454
|
-
const selectors = elements.map((el) => el.cssSelector);
|
|
2455
|
-
const matchCounts = await page.evaluate((sels) => {
|
|
2456
|
-
return sels.map((sel) => {
|
|
2457
|
-
try {
|
|
2458
|
-
return document.querySelectorAll(sel).length;
|
|
2459
|
-
}
|
|
2460
|
-
catch {
|
|
2461
|
-
return -1;
|
|
2462
|
-
}
|
|
2463
|
-
});
|
|
2464
|
-
}, selectors);
|
|
2465
|
-
const results = elements.map((el, i) => {
|
|
2466
|
-
const matchCount = matchCounts[i];
|
|
2467
|
-
let status;
|
|
2468
|
-
if (matchCount === -1) {
|
|
2469
|
-
status = 'invalid_selector';
|
|
2470
|
-
}
|
|
2471
|
-
else if (matchCount === 0) {
|
|
2472
|
-
status = 'not_found';
|
|
2473
|
-
}
|
|
2474
|
-
else if (matchCount === 1) {
|
|
2475
|
-
status = 'valid';
|
|
2476
|
-
}
|
|
2477
|
-
else {
|
|
2478
|
-
status = 'ambiguous';
|
|
2479
|
-
}
|
|
2480
|
-
return {
|
|
2481
|
-
ref: el.ref,
|
|
2482
|
-
index: el.index,
|
|
2483
|
-
cssSelector: el.cssSelector,
|
|
2484
|
-
status,
|
|
2485
|
-
matchCount,
|
|
2486
|
-
};
|
|
2487
|
-
});
|
|
2488
|
-
const failedCount = results.filter((r) => r.status === 'not_found' || r.status === 'invalid_selector').length;
|
|
2489
|
-
let newSnapshotId;
|
|
2490
|
-
if (failedCount > 0) {
|
|
2491
|
-
const newSnapshot = await browser.getSnapshot({ interactive: true });
|
|
2492
|
-
newSnapshotId = newSnapshot.snapshotId;
|
|
2493
|
-
}
|
|
2494
|
-
return successResponse(command.id, {
|
|
2495
|
-
snapshotId: command.target,
|
|
2496
|
-
results,
|
|
2497
|
-
newSnapshotId,
|
|
2498
|
-
});
|
|
2499
|
-
}
|
|
2500
|
-
async function handleFlowAction(command, browser) {
|
|
2501
|
-
const cmd = command;
|
|
2502
|
-
const subcommand = cmd.subcommand;
|
|
2503
|
-
switch (subcommand) {
|
|
2504
|
-
case 'run':
|
|
2505
|
-
return await handleFlowRun(cmd, browser);
|
|
2506
|
-
case 'list':
|
|
2507
|
-
return handleFlowList(cmd);
|
|
2508
|
-
case 'show':
|
|
2509
|
-
return handleFlowShow(cmd);
|
|
2510
|
-
case 'validate':
|
|
2511
|
-
return handleFlowValidate(cmd);
|
|
2512
|
-
case 'from-recorder':
|
|
2513
|
-
return handleFlowFromRecorder(cmd);
|
|
2514
|
-
case 'export':
|
|
2515
|
-
return handleFlowExport(cmd);
|
|
2516
|
-
default:
|
|
2517
|
-
return errorResponse(command.id, `Unknown flow subcommand: ${subcommand}`);
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
function handleFlowList(command) {
|
|
2521
|
-
const sites = command.sitesDir ? loadSitesFromDirectory(command.sitesDir) : loadAllSites();
|
|
2522
|
-
const siteList = [];
|
|
2523
|
-
for (const [name, site] of sites) {
|
|
2524
|
-
siteList.push({
|
|
2525
|
-
name,
|
|
2526
|
-
description: site.description,
|
|
2527
|
-
flows: Object.keys(site.flows),
|
|
2528
|
-
});
|
|
2529
|
-
}
|
|
2530
|
-
return successResponse(command.id, { sites: siteList });
|
|
2531
|
-
}
|
|
2532
|
-
function handleFlowShow(command) {
|
|
2533
|
-
const sites = command.sitesDir ? loadSitesFromDirectory(command.sitesDir) : loadAllSites();
|
|
2534
|
-
const ref = command.siteFlow || '';
|
|
2535
|
-
const result = findFlow(sites, ref);
|
|
2536
|
-
if (!result) {
|
|
2537
|
-
return errorResponse(command.id, `Flow "${ref}" not found`);
|
|
2538
|
-
}
|
|
2539
|
-
return successResponse(command.id, {
|
|
2540
|
-
site: {
|
|
2541
|
-
name: result.site.name,
|
|
2542
|
-
description: result.site.description,
|
|
2543
|
-
baseUrl: result.site.baseUrl,
|
|
2544
|
-
},
|
|
2545
|
-
flow: result.flow,
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
function handleFlowValidate(command) {
|
|
2549
|
-
const filePath = command.filePath || '';
|
|
2550
|
-
if (!filePath) {
|
|
2551
|
-
return errorResponse(command.id, 'Missing file path for validate');
|
|
2552
|
-
}
|
|
2553
|
-
const result = validateYamlFile(filePath);
|
|
2554
|
-
return successResponse(command.id, result);
|
|
2555
|
-
}
|
|
2556
|
-
function handleFlowFromRecorder(command) {
|
|
2557
|
-
const recorderFile = command.recorderFile;
|
|
2558
|
-
if (!recorderFile) {
|
|
2559
|
-
return errorResponse(command.id, 'Missing recorder YAML file path');
|
|
2560
|
-
}
|
|
2561
|
-
try {
|
|
2562
|
-
const result = recorderToFlowFromFile(recorderFile, {
|
|
2563
|
-
flowId: command.flowId,
|
|
2564
|
-
description: command.description,
|
|
2565
|
-
baseUrl: command.baseUrl,
|
|
2566
|
-
siteName: command.siteName,
|
|
2567
|
-
maxPaginateIterations: command.maxPaginateIterations,
|
|
2568
|
-
});
|
|
2569
|
-
const yamlString = siteToYamlString(result.site);
|
|
2570
|
-
if (command.outputFile) {
|
|
2571
|
-
writeFileSync(path.resolve(command.outputFile), yamlString, 'utf-8');
|
|
2572
|
-
return successResponse(command.id, {
|
|
2573
|
-
siteName: result.site.name,
|
|
2574
|
-
flowId: Object.keys(result.site.flows)[0],
|
|
2575
|
-
outputFile: command.outputFile,
|
|
2576
|
-
warnings: result.warnings,
|
|
2577
|
-
});
|
|
2578
|
-
}
|
|
2579
|
-
return successResponse(command.id, {
|
|
2580
|
-
siteName: result.site.name,
|
|
2581
|
-
flowId: Object.keys(result.site.flows)[0],
|
|
2582
|
-
yaml: yamlString,
|
|
2583
|
-
warnings: result.warnings,
|
|
2584
|
-
});
|
|
2585
|
-
}
|
|
2586
|
-
catch (e) {
|
|
2587
|
-
return errorResponse(command.id, `Failed to convert recorder YAML: ${e instanceof Error ? e.message : String(e)}`);
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
async function handleFlowRun(command, browser) {
|
|
2591
|
-
const ref = command.siteFlow || '';
|
|
2592
|
-
const sites = command.sitesDir ? loadSitesFromDirectory(command.sitesDir) : loadAllSites();
|
|
2593
|
-
const result = findFlow(sites, ref);
|
|
2594
|
-
if (!result) {
|
|
2595
|
-
return errorResponse(command.id, `Flow "${ref}" not found. Available sites: ${[...sites.keys()].join(', ')}`);
|
|
2596
|
-
}
|
|
2597
|
-
const typedParams = {};
|
|
2598
|
-
if (result.flow.params) {
|
|
2599
|
-
for (const param of result.flow.params) {
|
|
2600
|
-
const raw = command.params?.[param.name];
|
|
2601
|
-
if (raw !== undefined) {
|
|
2602
|
-
switch (param.type) {
|
|
2603
|
-
case 'number':
|
|
2604
|
-
typedParams[param.name] = Number(raw);
|
|
2605
|
-
break;
|
|
2606
|
-
case 'boolean':
|
|
2607
|
-
typedParams[param.name] = raw === 'true' || raw === '1';
|
|
2608
|
-
break;
|
|
2609
|
-
default:
|
|
2610
|
-
typedParams[param.name] = raw;
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
const executor = new FlowExecutor(browser);
|
|
2616
|
-
const flowResult = await executor.execute(result.site, result.flowName, typedParams);
|
|
2617
|
-
return successResponse(command.id, flowResult);
|
|
2618
|
-
}
|
|
2619
|
-
function handleFlowExport(command) {
|
|
2620
|
-
const filePath = command.filePath;
|
|
2621
|
-
if (!filePath) {
|
|
2622
|
-
return errorResponse(command.id, 'Missing file path for export');
|
|
2623
|
-
}
|
|
2624
|
-
let site;
|
|
2625
|
-
try {
|
|
2626
|
-
site = parseYamlSiteFile(filePath);
|
|
2627
|
-
}
|
|
2628
|
-
catch (e) {
|
|
2629
|
-
return errorResponse(command.id, `Failed to parse YAML file: ${e instanceof Error ? e.message : String(e)}`);
|
|
2630
|
-
}
|
|
2631
|
-
const flowEntries = Object.entries(site.flows);
|
|
2632
|
-
if (flowEntries.length === 0) {
|
|
2633
|
-
return errorResponse(command.id, 'No flows found in YAML file');
|
|
2634
|
-
}
|
|
2635
|
-
const flow = flowEntries[0][1];
|
|
2636
|
-
const format = command.format || 'playwright';
|
|
2637
|
-
const exporterMap = {
|
|
2638
|
-
playwright: new PlaywrightExporter(),
|
|
2639
|
-
python: new PythonExporter(),
|
|
2640
|
-
cypress: new CypressExporter(),
|
|
2641
|
-
selenium: new SeleniumExporter(),
|
|
2642
|
-
};
|
|
2643
|
-
const exporter = exporterMap[format];
|
|
2644
|
-
if (!exporter) {
|
|
2645
|
-
return errorResponse(command.id, `Unknown export format: "${format}". Available: ${Object.keys(exporterMap).join(', ')}`);
|
|
2646
|
-
}
|
|
2647
|
-
try {
|
|
2648
|
-
const script = exporter.export(flow.steps, {
|
|
2649
|
-
baseUrl: command.baseUrl || site.baseUrl,
|
|
2650
|
-
headless: command.headless,
|
|
2651
|
-
});
|
|
2652
|
-
return successResponse(command.id, {
|
|
2653
|
-
format: exporter.format,
|
|
2654
|
-
extension: exporter.extension,
|
|
2655
|
-
script,
|
|
2656
|
-
});
|
|
2657
|
-
}
|
|
2658
|
-
catch (e) {
|
|
2659
|
-
return errorResponse(command.id, `Export failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
//# sourceMappingURL=actions.js.map
|