speqs 0.5.1 → 0.7.0
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/commands/config.js +13 -4
- package/dist/commands/iteration.js +64 -13
- package/dist/commands/simulation.d.ts +1 -1
- package/dist/commands/simulation.js +454 -121
- package/dist/commands/study.js +140 -27
- package/dist/commands/tester-profile.js +17 -4
- package/dist/commands/tester.js +12 -3
- package/dist/commands/workspace.js +51 -6
- package/dist/config.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/lib/alias-store.d.ts +22 -3
- package/dist/lib/alias-store.js +60 -12
- package/dist/lib/api-client.d.ts +31 -0
- package/dist/lib/api-client.js +83 -27
- package/dist/lib/auth.js +4 -1
- package/dist/lib/command-helpers.d.ts +4 -0
- package/dist/lib/command-helpers.js +41 -4
- package/dist/lib/local-sim/actions.d.ts +22 -0
- package/dist/lib/local-sim/actions.js +379 -0
- package/dist/lib/local-sim/browser.d.ts +63 -0
- package/dist/lib/local-sim/browser.js +332 -0
- package/dist/lib/local-sim/debug-report.d.ts +21 -0
- package/dist/lib/local-sim/debug-report.js +186 -0
- package/dist/lib/local-sim/debug.d.ts +44 -0
- package/dist/lib/local-sim/debug.js +103 -0
- package/dist/lib/local-sim/install.d.ts +25 -0
- package/dist/lib/local-sim/install.js +72 -0
- package/dist/lib/local-sim/loop.d.ts +60 -0
- package/dist/lib/local-sim/loop.js +526 -0
- package/dist/lib/local-sim/types.d.ts +232 -0
- package/dist/lib/local-sim/types.js +8 -0
- package/dist/lib/local-sim/upload.d.ts +6 -0
- package/dist/lib/local-sim/upload.js +24 -0
- package/dist/lib/output.d.ts +16 -1
- package/dist/lib/output.js +250 -61
- package/dist/lib/types.d.ts +7 -30
- package/dist/lib/types.js +9 -1
- package/dist/lib/upload.d.ts +47 -0
- package/dist/lib/upload.js +178 -0
- package/package.json +3 -2
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action executor — resolves elements and executes Playwright actions.
|
|
3
|
+
*
|
|
4
|
+
* Resolution strategy:
|
|
5
|
+
* 1. CDP node resolution (using node_id from tree data)
|
|
6
|
+
* 2. Playwright locator fallback (using element_name + element_type)
|
|
7
|
+
* 3. Coordinate fallback (if returned by backend)
|
|
8
|
+
*/
|
|
9
|
+
import { resolveNodeToBoundingBox } from "./browser.js";
|
|
10
|
+
import { isDebugEnabled } from "./debug.js";
|
|
11
|
+
// --- ARIA role → Playwright role mapping ---
|
|
12
|
+
const ELEMENT_TYPE_TO_ROLE = {
|
|
13
|
+
BUTTON: "button",
|
|
14
|
+
TEXT_INPUT: "input",
|
|
15
|
+
SELECT: "combobox",
|
|
16
|
+
LINK: "link",
|
|
17
|
+
CHECKBOX: "checkbox",
|
|
18
|
+
RADIO: "radio",
|
|
19
|
+
HEADING: "heading",
|
|
20
|
+
TAB: "tab",
|
|
21
|
+
MENU_ITEM: "menuitem",
|
|
22
|
+
SWITCH: "switch",
|
|
23
|
+
SLIDER: "slider",
|
|
24
|
+
IMAGE: "img",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Execute a single action on the page.
|
|
28
|
+
*/
|
|
29
|
+
export async function executeAction(page, action, treeData, contextValues) {
|
|
30
|
+
try {
|
|
31
|
+
// Intercept "back button" taps — the LLM often tries to tap the browser
|
|
32
|
+
// back button which doesn't exist in the viewport. Convert to page.goBack().
|
|
33
|
+
const isBackAttempt = action.element_name?.toLowerCase().includes("back") &&
|
|
34
|
+
(action.element_description?.toLowerCase().includes("browser") ||
|
|
35
|
+
action.element_description?.toLowerCase().includes("navigate") ||
|
|
36
|
+
action.element_name?.toLowerCase().includes("browser back"));
|
|
37
|
+
if (isBackAttempt || action.type === "navigate_back") {
|
|
38
|
+
await page.goBack({ timeout: 10_000 }).catch(() => { });
|
|
39
|
+
return { success: true, elementName: action.element_name, coordinates: null };
|
|
40
|
+
}
|
|
41
|
+
let coordinates = null;
|
|
42
|
+
switch (action.type) {
|
|
43
|
+
case "tap":
|
|
44
|
+
coordinates = await executeTap(page, action, treeData);
|
|
45
|
+
break;
|
|
46
|
+
case "text_input":
|
|
47
|
+
coordinates = await executeTextInput(page, action, treeData, contextValues);
|
|
48
|
+
break;
|
|
49
|
+
case "scroll":
|
|
50
|
+
await executeScroll(page, action, treeData);
|
|
51
|
+
break;
|
|
52
|
+
case "swipe":
|
|
53
|
+
case "pull_to_refresh":
|
|
54
|
+
await executeSwipe(page, action.direction ?? "down");
|
|
55
|
+
break;
|
|
56
|
+
case "wait":
|
|
57
|
+
await page.waitForTimeout(action.duration_ms ?? 1000);
|
|
58
|
+
break;
|
|
59
|
+
case "navigate_back":
|
|
60
|
+
await page.goBack({ timeout: 10_000 }).catch(() => { });
|
|
61
|
+
break;
|
|
62
|
+
case "long_press":
|
|
63
|
+
coordinates = await executeLongPress(page, action, treeData);
|
|
64
|
+
break;
|
|
65
|
+
case "double_tap":
|
|
66
|
+
coordinates = await executeDoubleTap(page, action, treeData);
|
|
67
|
+
break;
|
|
68
|
+
case "drag":
|
|
69
|
+
// Drag requires coordinates — skip if unavailable
|
|
70
|
+
break;
|
|
71
|
+
case "think":
|
|
72
|
+
// No-op: model is reasoning without acting
|
|
73
|
+
break;
|
|
74
|
+
case "pinch_zoom":
|
|
75
|
+
case "rotate_device":
|
|
76
|
+
// Not supported in desktop browser
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
return { success: true, elementName: action.element_name, coordinates };
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (!isRecoverableError(err))
|
|
83
|
+
throw err;
|
|
84
|
+
return { success: false, elementName: action.element_name, coordinates: null };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// --- Element Resolution ---
|
|
88
|
+
/**
|
|
89
|
+
* Resolve an element: try CDP node_id first, then Playwright locator.
|
|
90
|
+
* Returns click coordinates { x, y } or null.
|
|
91
|
+
*/
|
|
92
|
+
async function resolveElement(page, action, treeData) {
|
|
93
|
+
// Strategy 1: CDP node resolution
|
|
94
|
+
if (action.node_id) {
|
|
95
|
+
const box = await resolveNodeToBoundingBox(page, action.node_id, treeData);
|
|
96
|
+
if (box) {
|
|
97
|
+
if (isDebugEnabled())
|
|
98
|
+
console.error(` [resolve] CDP node ${action.node_id} → (${Math.round(box.x)}, ${Math.round(box.y)})`);
|
|
99
|
+
return { x: box.x, y: box.y };
|
|
100
|
+
}
|
|
101
|
+
if (isDebugEnabled())
|
|
102
|
+
console.error(` [resolve] CDP node ${action.node_id} → FAILED, trying Playwright fallback`);
|
|
103
|
+
}
|
|
104
|
+
// Strategy 2: Playwright locator (tries multiple strategies)
|
|
105
|
+
const locator = await findElement(page, action);
|
|
106
|
+
if (locator) {
|
|
107
|
+
const box = await locator.boundingBox();
|
|
108
|
+
if (box) {
|
|
109
|
+
const coords = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
110
|
+
if (isDebugEnabled())
|
|
111
|
+
console.error(` [resolve] Playwright locator → (${Math.round(coords.x)}, ${Math.round(coords.y)})`);
|
|
112
|
+
return coords;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (isDebugEnabled())
|
|
116
|
+
console.error(` [resolve] ALL strategies FAILED for "${action.element_name}"`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve to a Playwright Locator (for fill/type operations that need a Locator).
|
|
121
|
+
*/
|
|
122
|
+
async function resolveLocator(page, action, treeData) {
|
|
123
|
+
return findElement(page, action);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build a Playwright locator, trying multiple strategies in order:
|
|
127
|
+
* 1. getByRole with element_name
|
|
128
|
+
* 2. getByRole with element_description (often more accurate)
|
|
129
|
+
* 3. getByText with element_name
|
|
130
|
+
* 4. getByText with element_description
|
|
131
|
+
*/
|
|
132
|
+
async function findElement(page, action) {
|
|
133
|
+
const role = action.element_type ? ELEMENT_TYPE_TO_ROLE[action.element_type] : null;
|
|
134
|
+
const name = action.element_name;
|
|
135
|
+
const desc = action.element_description;
|
|
136
|
+
const roleArg = role;
|
|
137
|
+
const candidates = [];
|
|
138
|
+
// Strategy 1: role + name
|
|
139
|
+
if (role && name) {
|
|
140
|
+
candidates.push(page.getByRole(roleArg, { name, exact: false }).first());
|
|
141
|
+
}
|
|
142
|
+
// Strategy 2: role + description
|
|
143
|
+
if (role && desc) {
|
|
144
|
+
candidates.push(page.getByRole(roleArg, { name: desc, exact: false }).first());
|
|
145
|
+
}
|
|
146
|
+
// Strategy 3: role only (if there's just one of that role)
|
|
147
|
+
if (role) {
|
|
148
|
+
candidates.push(page.getByRole(roleArg).first());
|
|
149
|
+
}
|
|
150
|
+
// Strategy 4: text search on name
|
|
151
|
+
if (name) {
|
|
152
|
+
candidates.push(page.getByText(name, { exact: false }).first());
|
|
153
|
+
}
|
|
154
|
+
// Strategy 5: text search on description
|
|
155
|
+
if (desc) {
|
|
156
|
+
candidates.push(page.getByText(desc, { exact: false }).first());
|
|
157
|
+
}
|
|
158
|
+
// Strategy 6: link by name (common case — LLM often calls links "buttons")
|
|
159
|
+
if (name) {
|
|
160
|
+
candidates.push(page.getByRole("link", { name, exact: false }).first());
|
|
161
|
+
}
|
|
162
|
+
if (desc) {
|
|
163
|
+
candidates.push(page.getByRole("link", { name: desc, exact: false }).first());
|
|
164
|
+
}
|
|
165
|
+
for (const locator of candidates) {
|
|
166
|
+
try {
|
|
167
|
+
await locator.waitFor({ state: "visible", timeout: 1500 });
|
|
168
|
+
return locator;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Try next strategy
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
// --- Action Implementations ---
|
|
177
|
+
async function executeTap(page, action, treeData) {
|
|
178
|
+
const count = action.count ?? 1;
|
|
179
|
+
const coords = await resolveElement(page, action, treeData);
|
|
180
|
+
if (coords) {
|
|
181
|
+
for (let i = 0; i < count; i++) {
|
|
182
|
+
await page.mouse.click(coords.x, coords.y);
|
|
183
|
+
}
|
|
184
|
+
return coords;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw new Error(`Cannot locate element for tap: ${action.element_name ?? "unknown"}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function executeTextInput(page, action, treeData, contextValues) {
|
|
191
|
+
// Resolve the actual text to type
|
|
192
|
+
const text = resolveTextValue(action, contextValues);
|
|
193
|
+
// Try to get a Playwright locator for fill operations
|
|
194
|
+
const locator = await resolveLocator(page, action, treeData);
|
|
195
|
+
if (locator) {
|
|
196
|
+
if (action.mode === "click_type") {
|
|
197
|
+
await locator.click({ timeout: 5000 });
|
|
198
|
+
await locator.fill("");
|
|
199
|
+
await locator.pressSequentially(text, { delay: 30 });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
await locator.fill(text);
|
|
203
|
+
}
|
|
204
|
+
if (action.submit) {
|
|
205
|
+
await locator.press("Enter");
|
|
206
|
+
}
|
|
207
|
+
// Extract coordinates from the locator for recording
|
|
208
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
209
|
+
return box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : null;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Coordinate-based fallback
|
|
213
|
+
const coords = await resolveElement(page, action, treeData);
|
|
214
|
+
if (coords) {
|
|
215
|
+
await page.mouse.click(coords.x, coords.y);
|
|
216
|
+
await page.waitForTimeout(200);
|
|
217
|
+
const selectAll = process.platform === "darwin" ? "Meta+a" : "Control+a";
|
|
218
|
+
await page.keyboard.press(selectAll);
|
|
219
|
+
await page.keyboard.type(text, { delay: 30 });
|
|
220
|
+
if (action.submit) {
|
|
221
|
+
await page.keyboard.press("Enter");
|
|
222
|
+
}
|
|
223
|
+
return coords;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
throw new Error(`Cannot locate element for text input: ${action.element_name ?? "unknown"}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function executeScroll(page, action, treeData) {
|
|
231
|
+
const viewport = page.viewportSize() ?? { width: 1440, height: 900 };
|
|
232
|
+
const amountMap = {
|
|
233
|
+
small: 0.5, medium: 0.8, large: 1.5, extra_large: 3.0,
|
|
234
|
+
};
|
|
235
|
+
const fraction = amountMap[action.amount ?? "medium"] ?? 0.8;
|
|
236
|
+
const pixels = Math.round(viewport.height * fraction);
|
|
237
|
+
switch (action.direction) {
|
|
238
|
+
case "up":
|
|
239
|
+
await page.evaluate((px) => window.scrollBy(0, -px), pixels);
|
|
240
|
+
break;
|
|
241
|
+
case "down":
|
|
242
|
+
await page.evaluate((px) => window.scrollBy(0, px), pixels);
|
|
243
|
+
break;
|
|
244
|
+
case "to_top":
|
|
245
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
246
|
+
break;
|
|
247
|
+
case "to_bottom":
|
|
248
|
+
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
|
|
249
|
+
break;
|
|
250
|
+
case "to_element": {
|
|
251
|
+
const locator = await findElement(page, action);
|
|
252
|
+
if (locator) {
|
|
253
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => { });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
await page.evaluate((px) => window.scrollBy(0, px), pixels);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
default:
|
|
261
|
+
await page.evaluate((px) => window.scrollBy(0, px), pixels);
|
|
262
|
+
}
|
|
263
|
+
await page.waitForTimeout(300);
|
|
264
|
+
}
|
|
265
|
+
async function executeSwipe(page, direction) {
|
|
266
|
+
const viewport = page.viewportSize() ?? { width: 1440, height: 900 };
|
|
267
|
+
const cx = viewport.width / 2;
|
|
268
|
+
const cy = viewport.height / 2;
|
|
269
|
+
const d = viewport.height * 0.4;
|
|
270
|
+
let sx = cx, sy = cy, ex = cx, ey = cy;
|
|
271
|
+
switch (direction) {
|
|
272
|
+
case "up":
|
|
273
|
+
sy = cy + d / 2;
|
|
274
|
+
ey = cy - d / 2;
|
|
275
|
+
break;
|
|
276
|
+
case "down":
|
|
277
|
+
sy = cy - d / 2;
|
|
278
|
+
ey = cy + d / 2;
|
|
279
|
+
break;
|
|
280
|
+
case "left":
|
|
281
|
+
sx = cx + d / 2;
|
|
282
|
+
ex = cx - d / 2;
|
|
283
|
+
break;
|
|
284
|
+
case "right":
|
|
285
|
+
sx = cx - d / 2;
|
|
286
|
+
ex = cx + d / 2;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
await page.mouse.move(sx, sy);
|
|
290
|
+
await page.mouse.down();
|
|
291
|
+
await page.mouse.move(ex, ey, { steps: 10 });
|
|
292
|
+
await page.mouse.up();
|
|
293
|
+
}
|
|
294
|
+
async function executeLongPress(page, action, treeData) {
|
|
295
|
+
const coords = await resolveElement(page, action, treeData);
|
|
296
|
+
if (!coords)
|
|
297
|
+
throw new Error(`Cannot locate element for long press: ${action.element_name ?? "unknown"}`);
|
|
298
|
+
await page.mouse.move(coords.x, coords.y);
|
|
299
|
+
await page.mouse.down();
|
|
300
|
+
await page.waitForTimeout(action.duration_ms ?? 500);
|
|
301
|
+
await page.mouse.up();
|
|
302
|
+
return coords;
|
|
303
|
+
}
|
|
304
|
+
async function executeDoubleTap(page, action, treeData) {
|
|
305
|
+
const coords = await resolveElement(page, action, treeData);
|
|
306
|
+
if (coords) {
|
|
307
|
+
await page.mouse.dblclick(coords.x, coords.y);
|
|
308
|
+
return coords;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
throw new Error(`Cannot locate element for double tap: ${action.element_name ?? "unknown"}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// --- Helpers ---
|
|
315
|
+
/**
|
|
316
|
+
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
317
|
+
*/
|
|
318
|
+
function resolveTextValue(action, contextValues) {
|
|
319
|
+
if (action.value_type === "var" || action.value_type === "secret") {
|
|
320
|
+
const cv = contextValues.find(v => v.name === action.value);
|
|
321
|
+
if (cv?.value)
|
|
322
|
+
return cv.value;
|
|
323
|
+
// Fallback to the key name if resolution fails
|
|
324
|
+
return action.value ?? "";
|
|
325
|
+
}
|
|
326
|
+
return action.value ?? "";
|
|
327
|
+
}
|
|
328
|
+
function isRecoverableError(err) {
|
|
329
|
+
if (err instanceof Error && err.constructor.name === "TargetClosedError")
|
|
330
|
+
return false;
|
|
331
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
332
|
+
const fatal = ["target page", "browser has been closed", "target closed", "browser disconnected"];
|
|
333
|
+
return !fatal.some((f) => msg.includes(f));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Compare two base64 screenshots to detect visible change.
|
|
337
|
+
*/
|
|
338
|
+
export function detectNoVisibleChange(before, after) {
|
|
339
|
+
return before === after;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Build a human-readable action description matching backend's format_action_detail().
|
|
343
|
+
*/
|
|
344
|
+
export function describeAction(action) {
|
|
345
|
+
const element = action.element_name || "element";
|
|
346
|
+
switch (action.type) {
|
|
347
|
+
case "tap":
|
|
348
|
+
return action.count && action.count > 1
|
|
349
|
+
? `tap on '${element}' x${action.count}`
|
|
350
|
+
: `tap on '${element}'`;
|
|
351
|
+
case "text_input": {
|
|
352
|
+
const val = action.value_type === "secret" ? "***" : `"${(action.value ?? "").slice(0, 30)}"`;
|
|
353
|
+
const modeStr = action.mode ? ` (${action.mode}${action.submit ? ", submit" : ""})` : "";
|
|
354
|
+
return `text_input on '${element}' → ${val}${modeStr}`;
|
|
355
|
+
}
|
|
356
|
+
case "scroll":
|
|
357
|
+
return action.direction === "to_element"
|
|
358
|
+
? `scroll to '${element}'`
|
|
359
|
+
: `scroll ${action.direction ?? "down"} (${action.amount ?? "medium"})`;
|
|
360
|
+
case "swipe":
|
|
361
|
+
return `swipe ${action.direction ?? "up"} on '${element}'`;
|
|
362
|
+
case "wait":
|
|
363
|
+
return `wait ${action.duration_ms ?? 1000}ms`;
|
|
364
|
+
case "navigate_back":
|
|
365
|
+
return "navigate back";
|
|
366
|
+
case "long_press":
|
|
367
|
+
return `long_press on '${element}'`;
|
|
368
|
+
case "double_tap":
|
|
369
|
+
return `double_tap on '${element}'`;
|
|
370
|
+
case "drag":
|
|
371
|
+
return `drag '${element}'`;
|
|
372
|
+
case "think":
|
|
373
|
+
return `think: "${(action.thoughts ?? "").slice(0, 50)}"`;
|
|
374
|
+
case "pull_to_refresh":
|
|
375
|
+
return "pull_to_refresh";
|
|
376
|
+
default:
|
|
377
|
+
return `${action.type} on '${element}'`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright browser lifecycle, CDP tree extraction, and node resolution.
|
|
3
|
+
*
|
|
4
|
+
* Tree format matches backend's get_accessibility_tree():
|
|
5
|
+
* [frame_index:node_id] role "name"
|
|
6
|
+
* with single-space indentation per level.
|
|
7
|
+
*/
|
|
8
|
+
import { type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
9
|
+
import type { LocalSimBrowserOptions, TreeData } from "./types.js";
|
|
10
|
+
import "./install.js";
|
|
11
|
+
export interface BrowserSession {
|
|
12
|
+
browser: Browser;
|
|
13
|
+
context: BrowserContext;
|
|
14
|
+
page: Page;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Launch a shared browser process with a pre-configured context.
|
|
18
|
+
* Create tabs with createTab() — they appear as tabs in one window.
|
|
19
|
+
*/
|
|
20
|
+
export declare function launchSharedBrowser(opts: LocalSimBrowserOptions): Promise<Browser>;
|
|
21
|
+
/**
|
|
22
|
+
* Create a new tab in the shared browser's default context.
|
|
23
|
+
* Tabs share cookies/storage — appears as tabs in one window in headed mode.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createTab(browser: Browser, opts: LocalSimBrowserOptions): Promise<BrowserSession>;
|
|
26
|
+
/**
|
|
27
|
+
* Launch a standalone browser session (single tester, owns the browser).
|
|
28
|
+
*/
|
|
29
|
+
export declare function launchBrowser(opts: LocalSimBrowserOptions): Promise<BrowserSession>;
|
|
30
|
+
export interface ObservationData {
|
|
31
|
+
screenshot: string;
|
|
32
|
+
treeData: TreeData;
|
|
33
|
+
url: string;
|
|
34
|
+
viewportWidth: number;
|
|
35
|
+
viewportHeight: number;
|
|
36
|
+
scrollPosition: number;
|
|
37
|
+
documentHeight: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Capture a full observation from the current page state.
|
|
41
|
+
*/
|
|
42
|
+
export declare function captureObservation(page: Page): Promise<ObservationData>;
|
|
43
|
+
/**
|
|
44
|
+
* Extract accessibility tree via CDP, matching backend's [nodeId] role "name" format.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractAccessibilityTree(page: Page): Promise<TreeData>;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a composite node_id (e.g., "0:42") to bounding box coordinates.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveNodeToBoundingBox(page: Page, nodeId: string, treeData: TreeData): Promise<{
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
} | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a composite node_id to an XPath selector.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveNodeToXPath(page: Page, nodeId: string, treeData: TreeData): Promise<string | null>;
|
|
60
|
+
export declare function takeScreenshot(page: Page): Promise<string>;
|
|
61
|
+
export declare function takeScreenshotJpeg(page: Page, quality?: number): Promise<Buffer>;
|
|
62
|
+
export declare function navigateWithRetry(page: Page, url: string, maxRetries?: number): Promise<void>;
|
|
63
|
+
export declare function closeBrowser(session: BrowserSession): Promise<void>;
|