btcp-browser-agent 0.1.0 → 0.1.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/package.json +8 -9
- package/packages/core/dist/actions.d.ts +97 -0
- package/packages/core/dist/actions.js +940 -0
- package/packages/core/dist/errors.d.ts +138 -0
- package/packages/core/dist/errors.js +157 -0
- package/packages/core/dist/index.d.ts +120 -0
- package/packages/core/dist/index.js +134 -0
- package/packages/core/dist/ref-map.d.ts +16 -0
- package/packages/core/dist/ref-map.js +91 -0
- package/packages/core/dist/snapshot.d.ts +37 -0
- package/packages/core/dist/snapshot.js +751 -0
- package/packages/core/dist/types.d.ts +396 -0
- package/packages/core/dist/types.js +7 -0
- package/packages/extension/dist/background.d.ts +227 -0
- package/packages/extension/dist/background.js +737 -0
- package/packages/extension/dist/content.d.ts +18 -0
- package/packages/extension/dist/content.js +149 -0
- package/packages/extension/dist/index.d.ts +228 -0
- package/packages/extension/dist/index.js +350 -0
- package/packages/extension/dist/session-manager.d.ts +87 -0
- package/packages/extension/dist/session-manager.js +322 -0
- package/packages/extension/{src/session-types.ts → dist/session-types.d.ts} +113 -144
- package/packages/extension/dist/session-types.js +5 -0
- package/packages/extension/dist/types.d.ts +88 -0
- package/packages/extension/dist/types.js +7 -0
- package/CLAUDE.md +0 -230
- package/SKILL.md +0 -143
- package/SNAPSHOT_IMPROVEMENTS.md +0 -302
- package/USAGE.md +0 -146
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/docs/browser-cli-design.md +0 -500
- package/examples/chrome-extension/CHANGELOG.md +0 -210
- package/examples/chrome-extension/DEBUG.md +0 -231
- package/examples/chrome-extension/ERROR_FIXED.md +0 -147
- package/examples/chrome-extension/QUICK_TEST.md +0 -189
- package/examples/chrome-extension/README.md +0 -149
- package/examples/chrome-extension/SESSION_ONLY_MODE.md +0 -305
- package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +0 -97
- package/examples/chrome-extension/build.js +0 -43
- package/examples/chrome-extension/manifest.json +0 -37
- package/examples/chrome-extension/package-lock.json +0 -1063
- package/examples/chrome-extension/package.json +0 -21
- package/examples/chrome-extension/popup.html +0 -195
- package/examples/chrome-extension/src/background.ts +0 -12
- package/examples/chrome-extension/src/content.ts +0 -7
- package/examples/chrome-extension/src/popup.ts +0 -303
- package/examples/chrome-extension/src/scenario-google-github.ts +0 -389
- package/examples/chrome-extension/test-page.html +0 -127
- package/examples/chrome-extension/tests/README.md +0 -206
- package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +0 -380
- package/examples/chrome-extension/tsconfig.json +0 -14
- package/examples/snapshots/README.md +0 -207
- package/examples/snapshots/amazon-com-detail.html +0 -9528
- package/examples/snapshots/amazon-com-detail.snapshot.txt +0 -997
- package/examples/snapshots/convert-snapshots.ts +0 -97
- package/examples/snapshots/edition-cnn-com.html +0 -13292
- package/examples/snapshots/edition-cnn-com.snapshot.txt +0 -562
- package/examples/snapshots/github-com-microsoft-vscode.html +0 -2916
- package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +0 -455
- package/examples/snapshots/google-search.html +0 -20012
- package/examples/snapshots/google-search.snapshot.txt +0 -195
- package/examples/snapshots/metadata.json +0 -86
- package/examples/snapshots/npr-org-templates.html +0 -2031
- package/examples/snapshots/npr-org-templates.snapshot.txt +0 -224
- package/examples/snapshots/stackoverflow-com.html +0 -5216
- package/examples/snapshots/stackoverflow-com.snapshot.txt +0 -2404
- package/examples/snapshots/test-all-mode.html +0 -46
- package/examples/snapshots/test-all-mode.snapshot.txt +0 -5
- package/examples/snapshots/validate.test.ts +0 -296
- package/packages/cli/package.json +0 -42
- package/packages/cli/src/__tests__/cli.test.ts +0 -434
- package/packages/cli/src/__tests__/errors.test.ts +0 -226
- package/packages/cli/src/__tests__/executor.test.ts +0 -275
- package/packages/cli/src/__tests__/formatter.test.ts +0 -260
- package/packages/cli/src/__tests__/parser.test.ts +0 -288
- package/packages/cli/src/__tests__/suggestions.test.ts +0 -255
- package/packages/cli/src/commands/back.ts +0 -22
- package/packages/cli/src/commands/check.ts +0 -33
- package/packages/cli/src/commands/clear.ts +0 -33
- package/packages/cli/src/commands/click.ts +0 -32
- package/packages/cli/src/commands/closetab.ts +0 -31
- package/packages/cli/src/commands/eval.ts +0 -41
- package/packages/cli/src/commands/fill.ts +0 -30
- package/packages/cli/src/commands/focus.ts +0 -33
- package/packages/cli/src/commands/forward.ts +0 -22
- package/packages/cli/src/commands/goto.ts +0 -34
- package/packages/cli/src/commands/help.ts +0 -162
- package/packages/cli/src/commands/hover.ts +0 -34
- package/packages/cli/src/commands/index.ts +0 -129
- package/packages/cli/src/commands/newtab.ts +0 -35
- package/packages/cli/src/commands/press.ts +0 -40
- package/packages/cli/src/commands/reload.ts +0 -25
- package/packages/cli/src/commands/screenshot.ts +0 -27
- package/packages/cli/src/commands/scroll.ts +0 -64
- package/packages/cli/src/commands/select.ts +0 -35
- package/packages/cli/src/commands/snapshot.ts +0 -21
- package/packages/cli/src/commands/tab.ts +0 -32
- package/packages/cli/src/commands/tabs.ts +0 -26
- package/packages/cli/src/commands/text.ts +0 -27
- package/packages/cli/src/commands/title.ts +0 -17
- package/packages/cli/src/commands/type.ts +0 -38
- package/packages/cli/src/commands/uncheck.ts +0 -33
- package/packages/cli/src/commands/url.ts +0 -17
- package/packages/cli/src/commands/wait.ts +0 -54
- package/packages/cli/src/errors.ts +0 -164
- package/packages/cli/src/executor.ts +0 -68
- package/packages/cli/src/formatter.ts +0 -215
- package/packages/cli/src/index.ts +0 -257
- package/packages/cli/src/parser.ts +0 -195
- package/packages/cli/src/suggestions.ts +0 -207
- package/packages/cli/src/terminal/Terminal.ts +0 -365
- package/packages/cli/src/terminal/index.ts +0 -5
- package/packages/cli/src/types.ts +0 -155
- package/packages/cli/tsconfig.json +0 -20
- package/packages/core/package.json +0 -35
- package/packages/core/src/actions.ts +0 -1210
- package/packages/core/src/errors.ts +0 -296
- package/packages/core/src/index.test.ts +0 -638
- package/packages/core/src/index.ts +0 -220
- package/packages/core/src/ref-map.ts +0 -107
- package/packages/core/src/snapshot.ts +0 -873
- package/packages/core/src/types.ts +0 -536
- package/packages/core/tsconfig.json +0 -23
- package/packages/extension/README.md +0 -129
- package/packages/extension/package.json +0 -43
- package/packages/extension/src/background.ts +0 -888
- package/packages/extension/src/content.ts +0 -172
- package/packages/extension/src/index.ts +0 -579
- package/packages/extension/src/session-manager.ts +0 -385
- package/packages/extension/src/types.ts +0 -162
- package/packages/extension/tsconfig.json +0 -28
- package/src/index.ts +0 -64
- package/tsconfig.build.json +0 -12
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -13
|
@@ -1,1210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @btcp/core - DOM Actions
|
|
3
|
-
*
|
|
4
|
-
* Element interaction handlers using native browser APIs.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
Command,
|
|
9
|
-
Response,
|
|
10
|
-
RefMap,
|
|
11
|
-
BoundingBox,
|
|
12
|
-
SnapshotData,
|
|
13
|
-
Modifier,
|
|
14
|
-
ValidateElementResponse,
|
|
15
|
-
ValidateRefsResponse,
|
|
16
|
-
} from './types.js';
|
|
17
|
-
import { createSnapshot } from './snapshot.js';
|
|
18
|
-
import {
|
|
19
|
-
DetailedError,
|
|
20
|
-
createElementNotFoundError,
|
|
21
|
-
createElementNotCompatibleError,
|
|
22
|
-
createTimeoutError,
|
|
23
|
-
createInvalidParametersError,
|
|
24
|
-
} from './errors.js';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* DOM Actions executor
|
|
28
|
-
*/
|
|
29
|
-
export class DOMActions {
|
|
30
|
-
private document: Document;
|
|
31
|
-
private window: Window;
|
|
32
|
-
private refMap: RefMap;
|
|
33
|
-
private lastSnapshotData: SnapshotData | null = null;
|
|
34
|
-
private overlayContainer: HTMLElement | null = null;
|
|
35
|
-
private scrollListener: (() => void) | null = null;
|
|
36
|
-
private rafId: number | null = null;
|
|
37
|
-
|
|
38
|
-
constructor(doc: Document, win: Window, refMap: RefMap) {
|
|
39
|
-
this.document = doc;
|
|
40
|
-
this.window = win;
|
|
41
|
-
this.refMap = refMap;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Execute a command and return a response
|
|
46
|
-
*/
|
|
47
|
-
async execute(command: Command): Promise<Response> {
|
|
48
|
-
try {
|
|
49
|
-
const data = await this.dispatch(command);
|
|
50
|
-
return { id: command.id, success: true, data };
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
|
|
54
|
-
// Include structured error data if available
|
|
55
|
-
if (error instanceof DetailedError) {
|
|
56
|
-
return {
|
|
57
|
-
id: command.id,
|
|
58
|
-
success: false,
|
|
59
|
-
error: message,
|
|
60
|
-
errorCode: error.code,
|
|
61
|
-
errorContext: error.context,
|
|
62
|
-
suggestions: error.suggestions,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return { id: command.id, success: false, error: message };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private async dispatch(command: Command): Promise<unknown> {
|
|
71
|
-
switch (command.action) {
|
|
72
|
-
case 'click':
|
|
73
|
-
return this.click(command.selector, {
|
|
74
|
-
button: command.button,
|
|
75
|
-
clickCount: command.clickCount,
|
|
76
|
-
modifiers: command.modifiers,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
case 'dblclick':
|
|
80
|
-
return this.dblclick(command.selector);
|
|
81
|
-
|
|
82
|
-
case 'type':
|
|
83
|
-
return this.type(command.selector, command.text, {
|
|
84
|
-
delay: command.delay,
|
|
85
|
-
clear: command.clear,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
case 'fill':
|
|
89
|
-
return this.fill(command.selector, command.value);
|
|
90
|
-
|
|
91
|
-
case 'clear':
|
|
92
|
-
return this.clear(command.selector);
|
|
93
|
-
|
|
94
|
-
case 'check':
|
|
95
|
-
return this.check(command.selector);
|
|
96
|
-
|
|
97
|
-
case 'uncheck':
|
|
98
|
-
return this.uncheck(command.selector);
|
|
99
|
-
|
|
100
|
-
case 'select':
|
|
101
|
-
return this.select(command.selector, command.values);
|
|
102
|
-
|
|
103
|
-
case 'focus':
|
|
104
|
-
return this.focus(command.selector);
|
|
105
|
-
|
|
106
|
-
case 'blur':
|
|
107
|
-
return this.blur(command.selector);
|
|
108
|
-
|
|
109
|
-
case 'hover':
|
|
110
|
-
return this.hover(command.selector);
|
|
111
|
-
|
|
112
|
-
case 'scroll':
|
|
113
|
-
return this.scroll(command.selector, {
|
|
114
|
-
x: command.x,
|
|
115
|
-
y: command.y,
|
|
116
|
-
direction: command.direction,
|
|
117
|
-
amount: command.amount,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
case 'scrollIntoView':
|
|
121
|
-
return this.scrollIntoView(command.selector, command.block);
|
|
122
|
-
|
|
123
|
-
case 'snapshot':
|
|
124
|
-
return this.snapshot({
|
|
125
|
-
selector: command.selector,
|
|
126
|
-
maxDepth: command.maxDepth,
|
|
127
|
-
includeHidden: command.includeHidden,
|
|
128
|
-
interactive: command.interactive,
|
|
129
|
-
compact: command.compact,
|
|
130
|
-
all: command.all,
|
|
131
|
-
format: command.format,
|
|
132
|
-
grep: command.grep,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
case 'querySelector':
|
|
136
|
-
return this.querySelector(command.selector);
|
|
137
|
-
|
|
138
|
-
case 'querySelectorAll':
|
|
139
|
-
return this.querySelectorAll(command.selector);
|
|
140
|
-
|
|
141
|
-
case 'getText':
|
|
142
|
-
return this.getText(command.selector);
|
|
143
|
-
|
|
144
|
-
case 'getAttribute':
|
|
145
|
-
return this.getAttribute(command.selector, command.attribute);
|
|
146
|
-
|
|
147
|
-
case 'getProperty':
|
|
148
|
-
return this.getProperty(command.selector, command.property);
|
|
149
|
-
|
|
150
|
-
case 'getBoundingBox':
|
|
151
|
-
return this.getBoundingBox(command.selector);
|
|
152
|
-
|
|
153
|
-
case 'isVisible':
|
|
154
|
-
return this.isVisible(command.selector);
|
|
155
|
-
|
|
156
|
-
case 'isEnabled':
|
|
157
|
-
return this.isEnabled(command.selector);
|
|
158
|
-
|
|
159
|
-
case 'isChecked':
|
|
160
|
-
return this.isChecked(command.selector);
|
|
161
|
-
|
|
162
|
-
case 'press':
|
|
163
|
-
return this.press(command.key, command.selector, command.modifiers);
|
|
164
|
-
|
|
165
|
-
case 'keyDown':
|
|
166
|
-
return this.keyDown(command.key);
|
|
167
|
-
|
|
168
|
-
case 'keyUp':
|
|
169
|
-
return this.keyUp(command.key);
|
|
170
|
-
|
|
171
|
-
case 'wait':
|
|
172
|
-
return this.wait(command.selector, {
|
|
173
|
-
state: command.state,
|
|
174
|
-
timeout: command.timeout,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
case 'evaluate':
|
|
178
|
-
return this.evaluate(command.script, command.args);
|
|
179
|
-
|
|
180
|
-
case 'validateElement':
|
|
181
|
-
return this.validateElement(command.selector, {
|
|
182
|
-
expectedType: command.expectedType,
|
|
183
|
-
capabilities: command.capabilities,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
case 'validateRefs':
|
|
187
|
-
return this.validateRefs(command.refs);
|
|
188
|
-
|
|
189
|
-
case 'highlight':
|
|
190
|
-
return this.highlight();
|
|
191
|
-
|
|
192
|
-
case 'clearHighlight':
|
|
193
|
-
return this.clearHighlight();
|
|
194
|
-
|
|
195
|
-
default:
|
|
196
|
-
throw new Error(`Unknown action: ${(command as Command).action}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// --- Element Resolution ---
|
|
201
|
-
|
|
202
|
-
private getElement(selector: string): Element {
|
|
203
|
-
const element = this.queryElement(selector);
|
|
204
|
-
if (!element) {
|
|
205
|
-
const isRef = selector.startsWith('@ref:');
|
|
206
|
-
const similarSelectors = isRef ? [] : this.findSimilarSelectors(selector);
|
|
207
|
-
const nearbyElements = this.getNearbyInteractiveElements();
|
|
208
|
-
|
|
209
|
-
throw createElementNotFoundError(selector, {
|
|
210
|
-
similarSelectors,
|
|
211
|
-
nearbyElements: nearbyElements.slice(0, 5),
|
|
212
|
-
isRef,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return element;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Find selectors similar to the given selector
|
|
220
|
-
*/
|
|
221
|
-
private findSimilarSelectors(selector: string): Array<{ selector: string; role: string; name: string }> {
|
|
222
|
-
const results: Array<{ selector: string; role: string; name: string }> = [];
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
// Try to extract ID or class from selector
|
|
226
|
-
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
227
|
-
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
228
|
-
|
|
229
|
-
if (idMatch) {
|
|
230
|
-
// Look for similar IDs
|
|
231
|
-
const targetId = idMatch[1].toLowerCase();
|
|
232
|
-
const allElements = this.document.querySelectorAll('[id]');
|
|
233
|
-
allElements.forEach(el => {
|
|
234
|
-
const elId = el.id.toLowerCase();
|
|
235
|
-
if (elId !== targetId && (elId.includes(targetId) || targetId.includes(elId))) {
|
|
236
|
-
const role = el.getAttribute('role') || el.tagName.toLowerCase();
|
|
237
|
-
const name = el.textContent?.trim().substring(0, 30) || el.getAttribute('aria-label') || '';
|
|
238
|
-
results.push({
|
|
239
|
-
selector: `#${el.id}`,
|
|
240
|
-
role,
|
|
241
|
-
name,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (classMatch && results.length < 3) {
|
|
248
|
-
// Look for similar classes
|
|
249
|
-
const targetClass = classMatch[1].toLowerCase();
|
|
250
|
-
const allElements = this.document.querySelectorAll('[class]');
|
|
251
|
-
allElements.forEach(el => {
|
|
252
|
-
const classes = Array.from(el.classList).map(c => c.toLowerCase());
|
|
253
|
-
const similarClass = classes.find(c => c !== targetClass && (c.includes(targetClass) || targetClass.includes(c)));
|
|
254
|
-
if (similarClass) {
|
|
255
|
-
const role = el.getAttribute('role') || el.tagName.toLowerCase();
|
|
256
|
-
const name = el.textContent?.trim().substring(0, 30) || el.getAttribute('aria-label') || '';
|
|
257
|
-
results.push({
|
|
258
|
-
selector: `.${similarClass}`,
|
|
259
|
-
role,
|
|
260
|
-
name,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
} catch (e) {
|
|
266
|
-
// Ignore errors in similarity search
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return results.slice(0, 3);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Get nearby interactive elements
|
|
274
|
-
*/
|
|
275
|
-
private getNearbyInteractiveElements(): Array<{ ref: string; role: string; name: string }> {
|
|
276
|
-
const results: Array<{ ref: string; role: string; name: string }> = [];
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const interactiveSelectors = [
|
|
280
|
-
'button',
|
|
281
|
-
'a[href]',
|
|
282
|
-
'input',
|
|
283
|
-
'textarea',
|
|
284
|
-
'select',
|
|
285
|
-
'[role="button"]',
|
|
286
|
-
'[role="link"]',
|
|
287
|
-
'[tabindex]'
|
|
288
|
-
];
|
|
289
|
-
|
|
290
|
-
const elements = this.document.querySelectorAll(interactiveSelectors.join(','));
|
|
291
|
-
|
|
292
|
-
elements.forEach(el => {
|
|
293
|
-
if (el instanceof HTMLElement) {
|
|
294
|
-
const style = this.window.getComputedStyle(el);
|
|
295
|
-
const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
|
|
296
|
-
|
|
297
|
-
if (isVisible) {
|
|
298
|
-
const ref = this.refMap.generateRef(el);
|
|
299
|
-
const role = el.getAttribute('role') || el.tagName.toLowerCase();
|
|
300
|
-
const name = el.textContent?.trim().substring(0, 30) ||
|
|
301
|
-
el.getAttribute('aria-label') ||
|
|
302
|
-
(el as HTMLInputElement).value?.substring(0, 30) ||
|
|
303
|
-
(el as HTMLInputElement).placeholder ||
|
|
304
|
-
'';
|
|
305
|
-
|
|
306
|
-
results.push({ ref, role, name });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
} catch (e) {
|
|
311
|
-
// Ignore errors in nearby element search
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return results.slice(0, 10);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Get available actions for an element based on its type
|
|
319
|
-
*/
|
|
320
|
-
private getAvailableActionsForElement(element: Element): string[] {
|
|
321
|
-
const actions: string[] = [];
|
|
322
|
-
|
|
323
|
-
// All elements can be queried and inspected
|
|
324
|
-
actions.push('querySelector', 'getText', 'getAttribute', 'getProperty', 'getBoundingBox', 'isVisible');
|
|
325
|
-
|
|
326
|
-
// Clickable elements
|
|
327
|
-
if (
|
|
328
|
-
element instanceof HTMLButtonElement ||
|
|
329
|
-
element instanceof HTMLAnchorElement ||
|
|
330
|
-
element.getAttribute('role') === 'button' ||
|
|
331
|
-
element.getAttribute('role') === 'link' ||
|
|
332
|
-
element.hasAttribute('onclick')
|
|
333
|
-
) {
|
|
334
|
-
actions.push('click', 'dblclick', 'hover');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Input elements
|
|
338
|
-
if (element instanceof HTMLInputElement) {
|
|
339
|
-
actions.push('fill', 'clear', 'focus', 'blur', 'isEnabled');
|
|
340
|
-
|
|
341
|
-
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
342
|
-
actions.push('check', 'uncheck', 'isChecked');
|
|
343
|
-
} else {
|
|
344
|
-
actions.push('type');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Textarea elements
|
|
349
|
-
if (element instanceof HTMLTextAreaElement) {
|
|
350
|
-
actions.push('type', 'fill', 'clear', 'focus', 'blur');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Select elements
|
|
354
|
-
if (element instanceof HTMLSelectElement) {
|
|
355
|
-
actions.push('select', 'focus', 'blur');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Focusable elements
|
|
359
|
-
if (element instanceof HTMLElement) {
|
|
360
|
-
actions.push('focus', 'blur', 'scroll', 'scrollIntoView', 'press');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return actions;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private queryElement(selector: string): Element | null {
|
|
367
|
-
// Check if it's a ref
|
|
368
|
-
if (selector.startsWith('@ref:')) {
|
|
369
|
-
return this.refMap.get(selector);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// CSS selector
|
|
373
|
-
return this.document.querySelector(selector);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private queryElements(selector: string): Element[] {
|
|
377
|
-
if (selector.startsWith('@ref:')) {
|
|
378
|
-
const el = this.refMap.get(selector);
|
|
379
|
-
return el ? [el] : [];
|
|
380
|
-
}
|
|
381
|
-
return Array.from(this.document.querySelectorAll(selector));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// --- Actions ---
|
|
385
|
-
|
|
386
|
-
private async click(
|
|
387
|
-
selector: string,
|
|
388
|
-
options: { button?: 'left' | 'right' | 'middle'; clickCount?: number; modifiers?: Modifier[] } = {}
|
|
389
|
-
): Promise<{ clicked: true }> {
|
|
390
|
-
const element = this.getElement(selector);
|
|
391
|
-
const { button = 'left', clickCount = 1, modifiers = [] } = options;
|
|
392
|
-
|
|
393
|
-
if (element instanceof HTMLElement) {
|
|
394
|
-
element.focus();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const buttonCode = button === 'right' ? 2 : button === 'middle' ? 1 : 0;
|
|
398
|
-
const eventInit: MouseEventInit = {
|
|
399
|
-
bubbles: true,
|
|
400
|
-
cancelable: true,
|
|
401
|
-
button: buttonCode,
|
|
402
|
-
altKey: modifiers.includes('Alt'),
|
|
403
|
-
ctrlKey: modifiers.includes('Control'),
|
|
404
|
-
metaKey: modifiers.includes('Meta'),
|
|
405
|
-
shiftKey: modifiers.includes('Shift'),
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
for (let i = 0; i < clickCount; i++) {
|
|
409
|
-
element.dispatchEvent(new MouseEvent('mousedown', eventInit));
|
|
410
|
-
element.dispatchEvent(new MouseEvent('mouseup', eventInit));
|
|
411
|
-
element.dispatchEvent(new MouseEvent('click', eventInit));
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return { clicked: true };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
private async dblclick(selector: string): Promise<{ clicked: true }> {
|
|
418
|
-
const element = this.getElement(selector);
|
|
419
|
-
element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
|
|
420
|
-
return { clicked: true };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private async type(
|
|
424
|
-
selector: string,
|
|
425
|
-
text: string,
|
|
426
|
-
options: { delay?: number; clear?: boolean } = {}
|
|
427
|
-
): Promise<{ typed: true }> {
|
|
428
|
-
const element = this.getElement(selector);
|
|
429
|
-
|
|
430
|
-
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
431
|
-
const actualType = element.tagName.toLowerCase();
|
|
432
|
-
const availableActions = this.getAvailableActionsForElement(element);
|
|
433
|
-
|
|
434
|
-
throw createElementNotCompatibleError(
|
|
435
|
-
selector,
|
|
436
|
-
'type',
|
|
437
|
-
actualType,
|
|
438
|
-
['input', 'textarea'],
|
|
439
|
-
availableActions
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
element.focus();
|
|
444
|
-
|
|
445
|
-
if (options.clear) {
|
|
446
|
-
element.value = '';
|
|
447
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
for (const char of text) {
|
|
451
|
-
element.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
452
|
-
element.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
453
|
-
element.value += char;
|
|
454
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
455
|
-
element.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
|
|
456
|
-
|
|
457
|
-
if (options.delay) {
|
|
458
|
-
await this.sleep(options.delay);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
463
|
-
return { typed: true };
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
private async fill(selector: string, value: string): Promise<{ filled: true }> {
|
|
467
|
-
const element = this.getElement(selector);
|
|
468
|
-
|
|
469
|
-
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
470
|
-
const actualType = element.tagName.toLowerCase();
|
|
471
|
-
const availableActions = this.getAvailableActionsForElement(element);
|
|
472
|
-
|
|
473
|
-
throw createElementNotCompatibleError(
|
|
474
|
-
selector,
|
|
475
|
-
'fill',
|
|
476
|
-
actualType,
|
|
477
|
-
['input', 'textarea'],
|
|
478
|
-
availableActions
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
element.focus();
|
|
483
|
-
element.value = value;
|
|
484
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
485
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
486
|
-
|
|
487
|
-
return { filled: true };
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
private async clear(selector: string): Promise<{ cleared: true }> {
|
|
491
|
-
const element = this.getElement(selector);
|
|
492
|
-
|
|
493
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
494
|
-
element.value = '';
|
|
495
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
496
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return { cleared: true };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
private async check(selector: string): Promise<{ checked: true }> {
|
|
503
|
-
const element = this.getElement(selector);
|
|
504
|
-
|
|
505
|
-
if (!(element instanceof HTMLInputElement)) {
|
|
506
|
-
const actualType = element.tagName.toLowerCase();
|
|
507
|
-
const availableActions = this.getAvailableActionsForElement(element);
|
|
508
|
-
|
|
509
|
-
throw createElementNotCompatibleError(
|
|
510
|
-
selector,
|
|
511
|
-
'check',
|
|
512
|
-
actualType,
|
|
513
|
-
['input[type=checkbox]', 'input[type=radio]'],
|
|
514
|
-
availableActions
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (!element.checked) {
|
|
519
|
-
element.click();
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return { checked: true };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
private async uncheck(selector: string): Promise<{ unchecked: true }> {
|
|
526
|
-
const element = this.getElement(selector);
|
|
527
|
-
|
|
528
|
-
if (!(element instanceof HTMLInputElement)) {
|
|
529
|
-
const actualType = element.tagName.toLowerCase();
|
|
530
|
-
const availableActions = this.getAvailableActionsForElement(element);
|
|
531
|
-
|
|
532
|
-
throw createElementNotCompatibleError(
|
|
533
|
-
selector,
|
|
534
|
-
'uncheck',
|
|
535
|
-
actualType,
|
|
536
|
-
['input[type=checkbox]'],
|
|
537
|
-
availableActions
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (element.checked) {
|
|
542
|
-
element.click();
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return { unchecked: true };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private async select(selector: string, values: string | string[]): Promise<{ selected: string[] }> {
|
|
549
|
-
const element = this.getElement(selector);
|
|
550
|
-
|
|
551
|
-
if (!(element instanceof HTMLSelectElement)) {
|
|
552
|
-
const actualType = element.tagName.toLowerCase();
|
|
553
|
-
const availableActions = this.getAvailableActionsForElement(element);
|
|
554
|
-
|
|
555
|
-
throw createElementNotCompatibleError(
|
|
556
|
-
selector,
|
|
557
|
-
'select',
|
|
558
|
-
actualType,
|
|
559
|
-
['select'],
|
|
560
|
-
availableActions
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const valueArray = Array.isArray(values) ? values : [values];
|
|
565
|
-
|
|
566
|
-
for (const option of element.options) {
|
|
567
|
-
option.selected = valueArray.includes(option.value);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
571
|
-
|
|
572
|
-
return { selected: valueArray };
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private async focus(selector: string): Promise<{ focused: true }> {
|
|
576
|
-
const element = this.getElement(selector);
|
|
577
|
-
|
|
578
|
-
if (element instanceof HTMLElement) {
|
|
579
|
-
element.focus();
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return { focused: true };
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
private async blur(selector: string): Promise<{ blurred: true }> {
|
|
586
|
-
const element = this.getElement(selector);
|
|
587
|
-
|
|
588
|
-
if (element instanceof HTMLElement) {
|
|
589
|
-
element.blur();
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return { blurred: true };
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private async hover(selector: string): Promise<{ hovered: true }> {
|
|
596
|
-
const element = this.getElement(selector);
|
|
597
|
-
|
|
598
|
-
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
599
|
-
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
600
|
-
|
|
601
|
-
return { hovered: true };
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
private async scroll(
|
|
605
|
-
selector: string | undefined,
|
|
606
|
-
options: { x?: number; y?: number; direction?: string; amount?: number }
|
|
607
|
-
): Promise<{ scrolled: true }> {
|
|
608
|
-
// Validate parameter combinations
|
|
609
|
-
const hasXY = options.x !== undefined || options.y !== undefined;
|
|
610
|
-
const hasDirection = options.direction !== undefined;
|
|
611
|
-
|
|
612
|
-
if (hasXY && hasDirection) {
|
|
613
|
-
throw createInvalidParametersError(
|
|
614
|
-
'Scroll command has conflicting parameters',
|
|
615
|
-
['x/y', 'direction'],
|
|
616
|
-
'Use either { x, y } for absolute scrolling OR { direction, amount } for relative scrolling, not both'
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
let deltaX = options.x ?? 0;
|
|
621
|
-
let deltaY = options.y ?? 0;
|
|
622
|
-
|
|
623
|
-
if (options.direction) {
|
|
624
|
-
const amount = options.amount ?? 100;
|
|
625
|
-
switch (options.direction) {
|
|
626
|
-
case 'up': deltaY = -amount; break;
|
|
627
|
-
case 'down': deltaY = amount; break;
|
|
628
|
-
case 'left': deltaX = -amount; break;
|
|
629
|
-
case 'right': deltaX = amount; break;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (selector) {
|
|
634
|
-
const element = this.getElement(selector);
|
|
635
|
-
element.scrollBy(deltaX, deltaY);
|
|
636
|
-
} else {
|
|
637
|
-
this.window.scrollBy(deltaX, deltaY);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return { scrolled: true };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
private async scrollIntoView(
|
|
644
|
-
selector: string,
|
|
645
|
-
block: 'start' | 'center' | 'end' | 'nearest' = 'center'
|
|
646
|
-
): Promise<{ scrolled: true }> {
|
|
647
|
-
const element = this.getElement(selector);
|
|
648
|
-
element.scrollIntoView({ behavior: 'smooth', block });
|
|
649
|
-
return { scrolled: true };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
private async snapshot(options: {
|
|
653
|
-
selector?: string;
|
|
654
|
-
maxDepth?: number;
|
|
655
|
-
includeHidden?: boolean;
|
|
656
|
-
interactive?: boolean;
|
|
657
|
-
compact?: boolean;
|
|
658
|
-
all?: boolean;
|
|
659
|
-
format?: 'tree' | 'html';
|
|
660
|
-
grep?: string | { pattern: string; ignoreCase?: boolean; invert?: boolean; fixedStrings?: boolean };
|
|
661
|
-
}): Promise<string> {
|
|
662
|
-
const root = options.selector
|
|
663
|
-
? this.getElement(options.selector)
|
|
664
|
-
: this.document.body;
|
|
665
|
-
|
|
666
|
-
const snapshotData = createSnapshot(this.document, this.refMap, {
|
|
667
|
-
root,
|
|
668
|
-
maxDepth: options.maxDepth,
|
|
669
|
-
includeHidden: options.includeHidden,
|
|
670
|
-
interactive: options.interactive,
|
|
671
|
-
compact: options.compact,
|
|
672
|
-
all: options.all,
|
|
673
|
-
format: options.format,
|
|
674
|
-
grep: options.grep,
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
// Store snapshot data for highlight command (preserve refs internally)
|
|
678
|
-
this.lastSnapshotData = snapshotData;
|
|
679
|
-
|
|
680
|
-
// Return only the tree string
|
|
681
|
-
return snapshotData.tree;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
private async querySelector(selector: string): Promise<{ found: boolean; ref?: string }> {
|
|
685
|
-
const element = this.queryElement(selector);
|
|
686
|
-
if (!element) {
|
|
687
|
-
return { found: false };
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const ref = this.refMap.generateRef(element);
|
|
691
|
-
return { found: true, ref };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
private async querySelectorAll(selector: string): Promise<{ count: number; refs: string[] }> {
|
|
695
|
-
const elements = this.queryElements(selector);
|
|
696
|
-
const refs = elements.map((el) => this.refMap.generateRef(el));
|
|
697
|
-
return { count: elements.length, refs };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
private async getText(selector: string): Promise<{ text: string | null }> {
|
|
701
|
-
const element = this.getElement(selector);
|
|
702
|
-
return { text: element.textContent };
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private async getAttribute(selector: string, attribute: string): Promise<{ value: string | null }> {
|
|
706
|
-
const element = this.getElement(selector);
|
|
707
|
-
return { value: element.getAttribute(attribute) };
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
private async getProperty(selector: string, property: string): Promise<{ value: unknown }> {
|
|
711
|
-
const element = this.getElement(selector);
|
|
712
|
-
return { value: (element as unknown as Record<string, unknown>)[property] };
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
private async getBoundingBox(selector: string): Promise<{ box: BoundingBox }> {
|
|
716
|
-
const element = this.getElement(selector);
|
|
717
|
-
const rect = element.getBoundingClientRect();
|
|
718
|
-
return {
|
|
719
|
-
box: {
|
|
720
|
-
x: rect.x,
|
|
721
|
-
y: rect.y,
|
|
722
|
-
width: rect.width,
|
|
723
|
-
height: rect.height,
|
|
724
|
-
},
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
private async isVisible(selector: string): Promise<{ visible: boolean }> {
|
|
729
|
-
const element = this.queryElement(selector);
|
|
730
|
-
if (!element || !(element instanceof HTMLElement)) {
|
|
731
|
-
return { visible: false };
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const style = this.window.getComputedStyle(element);
|
|
735
|
-
const visible =
|
|
736
|
-
style.display !== 'none' &&
|
|
737
|
-
style.visibility !== 'hidden' &&
|
|
738
|
-
style.opacity !== '0';
|
|
739
|
-
|
|
740
|
-
return { visible };
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
private async isEnabled(selector: string): Promise<{ enabled: boolean }> {
|
|
744
|
-
const element = this.getElement(selector);
|
|
745
|
-
const enabled = !(element as HTMLInputElement).disabled;
|
|
746
|
-
return { enabled };
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
private async isChecked(selector: string): Promise<{ checked: boolean }> {
|
|
750
|
-
const element = this.getElement(selector);
|
|
751
|
-
const checked = (element as HTMLInputElement).checked ?? false;
|
|
752
|
-
return { checked };
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
private async press(
|
|
756
|
-
key: string,
|
|
757
|
-
selector?: string,
|
|
758
|
-
modifiers: Modifier[] = []
|
|
759
|
-
): Promise<{ pressed: true }> {
|
|
760
|
-
const target = selector
|
|
761
|
-
? this.getElement(selector)
|
|
762
|
-
: this.document.activeElement || this.document.body;
|
|
763
|
-
|
|
764
|
-
const eventInit: KeyboardEventInit = {
|
|
765
|
-
key,
|
|
766
|
-
code: key,
|
|
767
|
-
bubbles: true,
|
|
768
|
-
cancelable: true,
|
|
769
|
-
altKey: modifiers.includes('Alt'),
|
|
770
|
-
ctrlKey: modifiers.includes('Control'),
|
|
771
|
-
metaKey: modifiers.includes('Meta'),
|
|
772
|
-
shiftKey: modifiers.includes('Shift'),
|
|
773
|
-
};
|
|
774
|
-
|
|
775
|
-
target.dispatchEvent(new KeyboardEvent('keydown', eventInit));
|
|
776
|
-
target.dispatchEvent(new KeyboardEvent('keypress', eventInit));
|
|
777
|
-
target.dispatchEvent(new KeyboardEvent('keyup', eventInit));
|
|
778
|
-
|
|
779
|
-
return { pressed: true };
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
private async keyDown(key: string): Promise<{ down: true }> {
|
|
783
|
-
const target = this.document.activeElement || this.document.body;
|
|
784
|
-
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
785
|
-
return { down: true };
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
private async keyUp(key: string): Promise<{ up: true }> {
|
|
789
|
-
const target = this.document.activeElement || this.document.body;
|
|
790
|
-
target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
|
|
791
|
-
return { up: true };
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
private async wait(
|
|
795
|
-
selector?: string,
|
|
796
|
-
options: { state?: string; timeout?: number } = {}
|
|
797
|
-
): Promise<{ waited: true }> {
|
|
798
|
-
const { state = 'visible', timeout = 5000 } = options;
|
|
799
|
-
|
|
800
|
-
if (!selector) {
|
|
801
|
-
await this.sleep(timeout);
|
|
802
|
-
return { waited: true };
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const startTime = Date.now();
|
|
806
|
-
let lastState: { attached: boolean; visible: boolean; enabled: boolean } | undefined;
|
|
807
|
-
|
|
808
|
-
while (Date.now() - startTime < timeout) {
|
|
809
|
-
const element = this.queryElement(selector);
|
|
810
|
-
|
|
811
|
-
// Track element state for error reporting
|
|
812
|
-
if (element instanceof HTMLElement) {
|
|
813
|
-
const style = this.window.getComputedStyle(element);
|
|
814
|
-
lastState = {
|
|
815
|
-
attached: true,
|
|
816
|
-
visible: style.display !== 'none' && style.visibility !== 'hidden',
|
|
817
|
-
enabled: !(element as HTMLInputElement).disabled,
|
|
818
|
-
};
|
|
819
|
-
} else if (element) {
|
|
820
|
-
lastState = {
|
|
821
|
-
attached: true,
|
|
822
|
-
visible: false,
|
|
823
|
-
enabled: true,
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
let conditionMet = false;
|
|
828
|
-
switch (state) {
|
|
829
|
-
case 'attached':
|
|
830
|
-
conditionMet = element !== null;
|
|
831
|
-
break;
|
|
832
|
-
case 'detached':
|
|
833
|
-
conditionMet = element === null;
|
|
834
|
-
break;
|
|
835
|
-
case 'visible':
|
|
836
|
-
if (element instanceof HTMLElement) {
|
|
837
|
-
const style = this.window.getComputedStyle(element);
|
|
838
|
-
conditionMet =
|
|
839
|
-
style.display !== 'none' &&
|
|
840
|
-
style.visibility !== 'hidden';
|
|
841
|
-
}
|
|
842
|
-
break;
|
|
843
|
-
case 'hidden':
|
|
844
|
-
conditionMet =
|
|
845
|
-
!element ||
|
|
846
|
-
(element instanceof HTMLElement &&
|
|
847
|
-
this.window.getComputedStyle(element).display === 'none');
|
|
848
|
-
break;
|
|
849
|
-
case 'enabled':
|
|
850
|
-
conditionMet = element !== null && !(element as HTMLInputElement).disabled;
|
|
851
|
-
break;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (conditionMet) {
|
|
855
|
-
return { waited: true };
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
await this.sleep(100);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// Provide detailed timeout error with current state
|
|
862
|
-
throw createTimeoutError(selector, state, lastState);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
private async evaluate(script: string, args?: unknown[]): Promise<{ result: unknown }> {
|
|
866
|
-
const fn = new Function(...(args?.map((_, i) => `arg${i}`) || []), `return (${script})`);
|
|
867
|
-
const result = fn.call(this.window, ...(args || []));
|
|
868
|
-
return { result };
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* Validate element capabilities before attempting an action
|
|
873
|
-
*/
|
|
874
|
-
private async validateElement(
|
|
875
|
-
selector: string,
|
|
876
|
-
options: {
|
|
877
|
-
expectedType?: 'input' | 'textarea' | 'button' | 'link' | 'select';
|
|
878
|
-
capabilities?: Array<'clickable' | 'editable' | 'checkable' | 'hoverable'>;
|
|
879
|
-
} = {}
|
|
880
|
-
): Promise<ValidateElementResponse> {
|
|
881
|
-
const element = this.getElement(selector);
|
|
882
|
-
const actualRole = element.getAttribute('role') || element.tagName.toLowerCase();
|
|
883
|
-
const actualType = element instanceof HTMLInputElement ? element.type : undefined;
|
|
884
|
-
|
|
885
|
-
// Get element capabilities
|
|
886
|
-
const capabilities = this.getAvailableActionsForElement(element);
|
|
887
|
-
|
|
888
|
-
// Get element state
|
|
889
|
-
const style = element instanceof HTMLElement ? this.window.getComputedStyle(element) : null;
|
|
890
|
-
const state = {
|
|
891
|
-
attached: true,
|
|
892
|
-
visible: style ? style.display !== 'none' && style.visibility !== 'hidden' : false,
|
|
893
|
-
enabled: !(element as HTMLInputElement).disabled,
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
// Check type compatibility
|
|
897
|
-
let compatible = true;
|
|
898
|
-
let suggestion: string | undefined;
|
|
899
|
-
|
|
900
|
-
if (options.expectedType) {
|
|
901
|
-
const typeMatch =
|
|
902
|
-
options.expectedType === actualRole ||
|
|
903
|
-
(options.expectedType === 'input' && element instanceof HTMLInputElement) ||
|
|
904
|
-
(options.expectedType === 'textarea' && element instanceof HTMLTextAreaElement) ||
|
|
905
|
-
(options.expectedType === 'button' && element instanceof HTMLButtonElement) ||
|
|
906
|
-
(options.expectedType === 'link' && element instanceof HTMLAnchorElement) ||
|
|
907
|
-
(options.expectedType === 'select' && element instanceof HTMLSelectElement);
|
|
908
|
-
|
|
909
|
-
if (!typeMatch) {
|
|
910
|
-
compatible = false;
|
|
911
|
-
suggestion = `Element is ${actualRole}, not ${options.expectedType}. Available actions: ${capabilities.slice(0, 5).join(', ')}`;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Check capability requirements
|
|
916
|
-
if (options.capabilities && compatible) {
|
|
917
|
-
const capabilityMap: Record<string, boolean> = {
|
|
918
|
-
clickable:
|
|
919
|
-
element instanceof HTMLButtonElement ||
|
|
920
|
-
element instanceof HTMLAnchorElement ||
|
|
921
|
-
element.getAttribute('role') === 'button' ||
|
|
922
|
-
element.hasAttribute('onclick'),
|
|
923
|
-
editable:
|
|
924
|
-
element instanceof HTMLInputElement ||
|
|
925
|
-
element instanceof HTMLTextAreaElement,
|
|
926
|
-
checkable:
|
|
927
|
-
element instanceof HTMLInputElement &&
|
|
928
|
-
(element.type === 'checkbox' || element.type === 'radio'),
|
|
929
|
-
hoverable: element instanceof HTMLElement,
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
for (const cap of options.capabilities) {
|
|
933
|
-
if (!capabilityMap[cap]) {
|
|
934
|
-
compatible = false;
|
|
935
|
-
suggestion = `Element does not support capability: ${cap}. Available actions: ${capabilities.slice(0, 5).join(', ')}`;
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return {
|
|
942
|
-
compatible,
|
|
943
|
-
actualRole,
|
|
944
|
-
actualType,
|
|
945
|
-
capabilities,
|
|
946
|
-
state,
|
|
947
|
-
suggestion,
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Validate that refs are still valid
|
|
953
|
-
*/
|
|
954
|
-
private async validateRefs(refs: string[]): Promise<ValidateRefsResponse> {
|
|
955
|
-
const valid: string[] = [];
|
|
956
|
-
const invalid: string[] = [];
|
|
957
|
-
const reasons: Record<string, string> = {};
|
|
958
|
-
|
|
959
|
-
for (const ref of refs) {
|
|
960
|
-
const element = this.refMap.get(ref);
|
|
961
|
-
|
|
962
|
-
if (element) {
|
|
963
|
-
// Check if element is still in the DOM
|
|
964
|
-
if (this.document.contains(element)) {
|
|
965
|
-
valid.push(ref);
|
|
966
|
-
} else {
|
|
967
|
-
invalid.push(ref);
|
|
968
|
-
reasons[ref] = 'Element has been removed from the DOM';
|
|
969
|
-
}
|
|
970
|
-
} else {
|
|
971
|
-
invalid.push(ref);
|
|
972
|
-
reasons[ref] = 'Ref not found. Refs are cleared on each snapshot() call.';
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return {
|
|
977
|
-
valid,
|
|
978
|
-
invalid,
|
|
979
|
-
reasons,
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Display visual overlay labels for interactive elements
|
|
985
|
-
*/
|
|
986
|
-
private async highlight(): Promise<{ highlighted: number }> {
|
|
987
|
-
// Verify snapshot exists
|
|
988
|
-
if (!this.lastSnapshotData || !this.lastSnapshotData.refs) {
|
|
989
|
-
throw new Error('No snapshot data available. Please run snapshot() command first.');
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// Clear any existing highlights
|
|
993
|
-
this.clearExistingOverlay();
|
|
994
|
-
|
|
995
|
-
// Create overlay container with absolute positioning covering entire document
|
|
996
|
-
this.overlayContainer = this.document.createElement('div');
|
|
997
|
-
this.overlayContainer.id = 'btcp-highlight-overlay';
|
|
998
|
-
this.overlayContainer.style.cssText = `
|
|
999
|
-
position: absolute;
|
|
1000
|
-
top: 0;
|
|
1001
|
-
left: 0;
|
|
1002
|
-
width: ${this.document.documentElement.scrollWidth}px;
|
|
1003
|
-
height: ${this.document.documentElement.scrollHeight}px;
|
|
1004
|
-
pointer-events: none;
|
|
1005
|
-
z-index: 999999;
|
|
1006
|
-
contain: layout style paint;
|
|
1007
|
-
`;
|
|
1008
|
-
|
|
1009
|
-
let highlightedCount = 0;
|
|
1010
|
-
|
|
1011
|
-
// Create border overlays and labels for each ref
|
|
1012
|
-
for (const [ref, _refData] of Object.entries(this.lastSnapshotData.refs)) {
|
|
1013
|
-
const element = this.refMap.get(ref);
|
|
1014
|
-
|
|
1015
|
-
// Skip if element no longer exists or is disconnected
|
|
1016
|
-
if (!element || !element.isConnected) {
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
try {
|
|
1021
|
-
// Get current bounding box (element might have moved)
|
|
1022
|
-
const bbox = element.getBoundingClientRect();
|
|
1023
|
-
|
|
1024
|
-
// Skip elements with no dimensions
|
|
1025
|
-
if (bbox.width === 0 && bbox.height === 0) {
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Create border overlay
|
|
1030
|
-
const border = this.document.createElement('div');
|
|
1031
|
-
border.className = 'btcp-ref-border';
|
|
1032
|
-
border.dataset.ref = ref;
|
|
1033
|
-
border.style.cssText = `
|
|
1034
|
-
position: absolute;
|
|
1035
|
-
width: ${bbox.width}px;
|
|
1036
|
-
height: ${bbox.height}px;
|
|
1037
|
-
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
1038
|
-
border: 2px solid rgba(59, 130, 246, 0.8);
|
|
1039
|
-
border-radius: 2px;
|
|
1040
|
-
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
|
|
1041
|
-
pointer-events: none;
|
|
1042
|
-
will-change: transform;
|
|
1043
|
-
contain: layout style paint;
|
|
1044
|
-
`;
|
|
1045
|
-
|
|
1046
|
-
// Create label
|
|
1047
|
-
const label = this.document.createElement('div');
|
|
1048
|
-
label.className = 'btcp-ref-label';
|
|
1049
|
-
label.dataset.ref = ref;
|
|
1050
|
-
// Extract number from ref (e.g., "@ref:5" -> "5")
|
|
1051
|
-
label.textContent = ref.replace('@ref:', '');
|
|
1052
|
-
label.style.cssText = `
|
|
1053
|
-
position: absolute;
|
|
1054
|
-
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
1055
|
-
background: rgba(59, 130, 246, 0.9);
|
|
1056
|
-
color: white;
|
|
1057
|
-
padding: 2px 6px;
|
|
1058
|
-
border-radius: 3px;
|
|
1059
|
-
font-family: monospace;
|
|
1060
|
-
font-size: 11px;
|
|
1061
|
-
font-weight: bold;
|
|
1062
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
1063
|
-
pointer-events: none;
|
|
1064
|
-
white-space: nowrap;
|
|
1065
|
-
will-change: transform;
|
|
1066
|
-
contain: layout style paint;
|
|
1067
|
-
`;
|
|
1068
|
-
|
|
1069
|
-
this.overlayContainer.appendChild(border);
|
|
1070
|
-
this.overlayContainer.appendChild(label);
|
|
1071
|
-
highlightedCount++;
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
// Skip elements that throw errors
|
|
1074
|
-
continue;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Inject overlay into page
|
|
1079
|
-
this.document.body.appendChild(this.overlayContainer);
|
|
1080
|
-
|
|
1081
|
-
// Set up scroll listener with rAF throttling
|
|
1082
|
-
let ticking = false;
|
|
1083
|
-
this.scrollListener = () => {
|
|
1084
|
-
if (!ticking) {
|
|
1085
|
-
this.rafId = this.window.requestAnimationFrame(() => {
|
|
1086
|
-
this.updateHighlightPositions();
|
|
1087
|
-
ticking = false;
|
|
1088
|
-
});
|
|
1089
|
-
ticking = true;
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
|
|
1093
|
-
// Use passive listener for better scroll performance
|
|
1094
|
-
this.window.addEventListener('scroll', this.scrollListener, { passive: true });
|
|
1095
|
-
|
|
1096
|
-
return { highlighted: highlightedCount };
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Invalidate snapshot data (called on navigation or manual clear)
|
|
1101
|
-
*/
|
|
1102
|
-
public invalidateSnapshot(): void {
|
|
1103
|
-
this.lastSnapshotData = null;
|
|
1104
|
-
this.clearHighlight();
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Update highlight positions on scroll (GPU-accelerated)
|
|
1109
|
-
*/
|
|
1110
|
-
private updateHighlightPositions(): void {
|
|
1111
|
-
if (!this.overlayContainer || !this.lastSnapshotData) {
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// Batch DOM reads
|
|
1116
|
-
const updates: Array<{ element: HTMLElement; x: number; y: number; width?: number; height?: number }> = [];
|
|
1117
|
-
|
|
1118
|
-
// Read phase - get all bounding boxes first
|
|
1119
|
-
const borders = this.overlayContainer.querySelectorAll('.btcp-ref-border');
|
|
1120
|
-
const labels = this.overlayContainer.querySelectorAll('.btcp-ref-label');
|
|
1121
|
-
|
|
1122
|
-
borders.forEach((borderEl) => {
|
|
1123
|
-
const ref = (borderEl as HTMLElement).dataset.ref;
|
|
1124
|
-
if (!ref) return;
|
|
1125
|
-
|
|
1126
|
-
const element = this.refMap.get(ref);
|
|
1127
|
-
if (!element || !element.isConnected) return;
|
|
1128
|
-
|
|
1129
|
-
const bbox = element.getBoundingClientRect();
|
|
1130
|
-
if (bbox.width === 0 && bbox.height === 0) return;
|
|
1131
|
-
|
|
1132
|
-
updates.push({
|
|
1133
|
-
element: borderEl as HTMLElement,
|
|
1134
|
-
x: bbox.left + this.window.scrollX,
|
|
1135
|
-
y: bbox.top + this.window.scrollY,
|
|
1136
|
-
width: bbox.width,
|
|
1137
|
-
height: bbox.height,
|
|
1138
|
-
});
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
labels.forEach((labelEl) => {
|
|
1142
|
-
const ref = (labelEl as HTMLElement).dataset.ref;
|
|
1143
|
-
if (!ref) return;
|
|
1144
|
-
|
|
1145
|
-
const element = this.refMap.get(ref);
|
|
1146
|
-
if (!element || !element.isConnected) return;
|
|
1147
|
-
|
|
1148
|
-
const bbox = element.getBoundingClientRect();
|
|
1149
|
-
if (bbox.width === 0 && bbox.height === 0) return;
|
|
1150
|
-
|
|
1151
|
-
updates.push({
|
|
1152
|
-
element: labelEl as HTMLElement,
|
|
1153
|
-
x: bbox.left + this.window.scrollX,
|
|
1154
|
-
y: bbox.top + this.window.scrollY,
|
|
1155
|
-
});
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
// Write phase - update transforms
|
|
1159
|
-
updates.forEach(({ element, x, y, width, height }) => {
|
|
1160
|
-
element.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
|
1161
|
-
if (width !== undefined && height !== undefined) {
|
|
1162
|
-
element.style.width = `${width}px`;
|
|
1163
|
-
element.style.height = `${height}px`;
|
|
1164
|
-
}
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
/**
|
|
1169
|
-
* Remove visual overlay labels
|
|
1170
|
-
*/
|
|
1171
|
-
private async clearHighlight(): Promise<{ cleared: true }> {
|
|
1172
|
-
this.clearExistingOverlay();
|
|
1173
|
-
return { cleared: true };
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* Remove existing overlay if it exists
|
|
1178
|
-
*/
|
|
1179
|
-
private clearExistingOverlay(): void {
|
|
1180
|
-
// Remove scroll listener
|
|
1181
|
-
if (this.scrollListener) {
|
|
1182
|
-
this.window.removeEventListener('scroll', this.scrollListener);
|
|
1183
|
-
this.scrollListener = null;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Cancel any pending animation frame
|
|
1187
|
-
if (this.rafId !== null) {
|
|
1188
|
-
this.window.cancelAnimationFrame(this.rafId);
|
|
1189
|
-
this.rafId = null;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// Remove overlay container
|
|
1193
|
-
if (this.overlayContainer && this.overlayContainer.parentNode) {
|
|
1194
|
-
this.overlayContainer.parentNode.removeChild(this.overlayContainer);
|
|
1195
|
-
}
|
|
1196
|
-
this.overlayContainer = null;
|
|
1197
|
-
|
|
1198
|
-
// Also remove any orphaned overlays
|
|
1199
|
-
const existingOverlay = this.document.getElementById('btcp-highlight-overlay');
|
|
1200
|
-
if (existingOverlay && existingOverlay.parentNode) {
|
|
1201
|
-
existingOverlay.parentNode.removeChild(existingOverlay);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// --- Utilities ---
|
|
1206
|
-
|
|
1207
|
-
private sleep(ms: number): Promise<void> {
|
|
1208
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1209
|
-
}
|
|
1210
|
-
}
|