@predicatelabs/sdk 0.99.9
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/LICENSE +24 -0
- package/README.md +252 -0
- package/dist/actions.d.ts +185 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +1120 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-runtime.d.ts +352 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +1170 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent.d.ts +164 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +408 -0
- package/dist/agent.js.map +1 -0
- package/dist/asserts/expect.d.ts +159 -0
- package/dist/asserts/expect.d.ts.map +1 -0
- package/dist/asserts/expect.js +547 -0
- package/dist/asserts/expect.js.map +1 -0
- package/dist/asserts/index.d.ts +58 -0
- package/dist/asserts/index.d.ts.map +1 -0
- package/dist/asserts/index.js +70 -0
- package/dist/asserts/index.js.map +1 -0
- package/dist/asserts/query.d.ts +199 -0
- package/dist/asserts/query.d.ts.map +1 -0
- package/dist/asserts/query.js +288 -0
- package/dist/asserts/query.js.map +1 -0
- package/dist/backends/actions.d.ts +119 -0
- package/dist/backends/actions.d.ts.map +1 -0
- package/dist/backends/actions.js +291 -0
- package/dist/backends/actions.js.map +1 -0
- package/dist/backends/browser-use-adapter.d.ts +131 -0
- package/dist/backends/browser-use-adapter.d.ts.map +1 -0
- package/dist/backends/browser-use-adapter.js +219 -0
- package/dist/backends/browser-use-adapter.js.map +1 -0
- package/dist/backends/cdp-backend.d.ts +66 -0
- package/dist/backends/cdp-backend.d.ts.map +1 -0
- package/dist/backends/cdp-backend.js +273 -0
- package/dist/backends/cdp-backend.js.map +1 -0
- package/dist/backends/index.d.ts +80 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +101 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/protocol.d.ts +156 -0
- package/dist/backends/protocol.d.ts.map +1 -0
- package/dist/backends/protocol.js +16 -0
- package/dist/backends/protocol.js.map +1 -0
- package/dist/backends/sentience-context.d.ts +143 -0
- package/dist/backends/sentience-context.d.ts.map +1 -0
- package/dist/backends/sentience-context.js +359 -0
- package/dist/backends/sentience-context.js.map +1 -0
- package/dist/backends/snapshot.d.ts +188 -0
- package/dist/backends/snapshot.d.ts.map +1 -0
- package/dist/backends/snapshot.js +360 -0
- package/dist/backends/snapshot.js.map +1 -0
- package/dist/browser.d.ts +154 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +920 -0
- package/dist/browser.js.map +1 -0
- package/dist/canonicalization.d.ts +126 -0
- package/dist/canonicalization.d.ts.map +1 -0
- package/dist/canonicalization.js +161 -0
- package/dist/canonicalization.js.map +1 -0
- package/dist/captcha/strategies.d.ts +12 -0
- package/dist/captcha/strategies.d.ts.map +1 -0
- package/dist/captcha/strategies.js +43 -0
- package/dist/captcha/strategies.js.map +1 -0
- package/dist/captcha/types.d.ts +45 -0
- package/dist/captcha/types.d.ts.map +1 -0
- package/dist/captcha/types.js +12 -0
- package/dist/captcha/types.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +422 -0
- package/dist/cli.js.map +1 -0
- package/dist/conversational-agent.d.ts +123 -0
- package/dist/conversational-agent.d.ts.map +1 -0
- package/dist/conversational-agent.js +341 -0
- package/dist/conversational-agent.js.map +1 -0
- package/dist/cursor-policy.d.ts +41 -0
- package/dist/cursor-policy.d.ts.map +1 -0
- package/dist/cursor-policy.js +81 -0
- package/dist/cursor-policy.js.map +1 -0
- package/dist/debugger.d.ts +28 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +107 -0
- package/dist/debugger.js.map +1 -0
- package/dist/expect.d.ts +16 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +67 -0
- package/dist/expect.js.map +1 -0
- package/dist/failure-artifacts.d.ts +95 -0
- package/dist/failure-artifacts.d.ts.map +1 -0
- package/dist/failure-artifacts.js +805 -0
- package/dist/failure-artifacts.js.map +1 -0
- package/dist/generator.d.ts +16 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +205 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector.d.ts +13 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/inspector.js +153 -0
- package/dist/inspector.js.map +1 -0
- package/dist/llm-provider.d.ts +144 -0
- package/dist/llm-provider.d.ts.map +1 -0
- package/dist/llm-provider.js +460 -0
- package/dist/llm-provider.js.map +1 -0
- package/dist/ordinal.d.ts +90 -0
- package/dist/ordinal.d.ts.map +1 -0
- package/dist/ordinal.js +249 -0
- package/dist/ordinal.js.map +1 -0
- package/dist/overlay.d.ts +63 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +102 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocols/browser-protocol.d.ts +79 -0
- package/dist/protocols/browser-protocol.d.ts.map +1 -0
- package/dist/protocols/browser-protocol.js +9 -0
- package/dist/protocols/browser-protocol.js.map +1 -0
- package/dist/query.d.ts +66 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +482 -0
- package/dist/query.js.map +1 -0
- package/dist/read.d.ts +47 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +128 -0
- package/dist/read.js.map +1 -0
- package/dist/recorder.d.ts +44 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +262 -0
- package/dist/recorder.js.map +1 -0
- package/dist/runtime-agent.d.ts +72 -0
- package/dist/runtime-agent.d.ts.map +1 -0
- package/dist/runtime-agent.js +357 -0
- package/dist/runtime-agent.js.map +1 -0
- package/dist/screenshot.d.ts +17 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +40 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/snapshot-diff.d.ts +23 -0
- package/dist/snapshot-diff.d.ts.map +1 -0
- package/dist/snapshot-diff.js +119 -0
- package/dist/snapshot-diff.js.map +1 -0
- package/dist/snapshot.d.ts +47 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +358 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/textSearch.d.ts +64 -0
- package/dist/textSearch.d.ts.map +1 -0
- package/dist/textSearch.js +113 -0
- package/dist/textSearch.js.map +1 -0
- package/dist/tools/context.d.ts +18 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +40 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/defaults.d.ts +5 -0
- package/dist/tools/defaults.d.ts.map +1 -0
- package/dist/tools/defaults.js +368 -0
- package/dist/tools/defaults.js.map +1 -0
- package/dist/tools/filesystem.d.ts +12 -0
- package/dist/tools/filesystem.d.ts.map +1 -0
- package/dist/tools/filesystem.js +137 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +100 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tracing/cloud-sink.d.ts +189 -0
- package/dist/tracing/cloud-sink.d.ts.map +1 -0
- package/dist/tracing/cloud-sink.js +1067 -0
- package/dist/tracing/cloud-sink.js.map +1 -0
- package/dist/tracing/index-schema.d.ts +231 -0
- package/dist/tracing/index-schema.d.ts.map +1 -0
- package/dist/tracing/index-schema.js +235 -0
- package/dist/tracing/index-schema.js.map +1 -0
- package/dist/tracing/index.d.ts +12 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +28 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/tracing/indexer.d.ts +20 -0
- package/dist/tracing/indexer.d.ts.map +1 -0
- package/dist/tracing/indexer.js +347 -0
- package/dist/tracing/indexer.js.map +1 -0
- package/dist/tracing/jsonl-sink.d.ts +51 -0
- package/dist/tracing/jsonl-sink.d.ts.map +1 -0
- package/dist/tracing/jsonl-sink.js +329 -0
- package/dist/tracing/jsonl-sink.js.map +1 -0
- package/dist/tracing/sink.d.ts +25 -0
- package/dist/tracing/sink.d.ts.map +1 -0
- package/dist/tracing/sink.js +15 -0
- package/dist/tracing/sink.js.map +1 -0
- package/dist/tracing/tracer-factory.d.ts +102 -0
- package/dist/tracing/tracer-factory.d.ts.map +1 -0
- package/dist/tracing/tracer-factory.js +375 -0
- package/dist/tracing/tracer-factory.js.map +1 -0
- package/dist/tracing/tracer.d.ts +140 -0
- package/dist/tracing/tracer.d.ts.map +1 -0
- package/dist/tracing/tracer.js +336 -0
- package/dist/tracing/tracer.js.map +1 -0
- package/dist/tracing/types.d.ts +203 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +8 -0
- package/dist/tracing/types.js.map +1 -0
- package/dist/types.d.ts +422 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/action-executor.d.ts +25 -0
- package/dist/utils/action-executor.d.ts.map +1 -0
- package/dist/utils/action-executor.js +121 -0
- package/dist/utils/action-executor.js.map +1 -0
- package/dist/utils/browser-evaluator.d.ts +76 -0
- package/dist/utils/browser-evaluator.d.ts.map +1 -0
- package/dist/utils/browser-evaluator.js +130 -0
- package/dist/utils/browser-evaluator.js.map +1 -0
- package/dist/utils/browser.d.ts +30 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +75 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/element-filter.d.ts +76 -0
- package/dist/utils/element-filter.d.ts.map +1 -0
- package/dist/utils/element-filter.js +195 -0
- package/dist/utils/element-filter.js.map +1 -0
- package/dist/utils/grid-utils.d.ts +37 -0
- package/dist/utils/grid-utils.d.ts.map +1 -0
- package/dist/utils/grid-utils.js +283 -0
- package/dist/utils/grid-utils.js.map +1 -0
- package/dist/utils/llm-interaction-handler.d.ts +41 -0
- package/dist/utils/llm-interaction-handler.d.ts.map +1 -0
- package/dist/utils/llm-interaction-handler.js +171 -0
- package/dist/utils/llm-interaction-handler.js.map +1 -0
- package/dist/utils/llm-response-builder.d.ts +56 -0
- package/dist/utils/llm-response-builder.d.ts.map +1 -0
- package/dist/utils/llm-response-builder.js +130 -0
- package/dist/utils/llm-response-builder.js.map +1 -0
- package/dist/utils/selector-utils.d.ts +12 -0
- package/dist/utils/selector-utils.d.ts.map +1 -0
- package/dist/utils/selector-utils.js +32 -0
- package/dist/utils/selector-utils.js.map +1 -0
- package/dist/utils/snapshot-event-builder.d.ts +28 -0
- package/dist/utils/snapshot-event-builder.d.ts.map +1 -0
- package/dist/utils/snapshot-event-builder.js +88 -0
- package/dist/utils/snapshot-event-builder.js.map +1 -0
- package/dist/utils/snapshot-processor.d.ts +27 -0
- package/dist/utils/snapshot-processor.d.ts.map +1 -0
- package/dist/utils/snapshot-processor.js +47 -0
- package/dist/utils/snapshot-processor.js.map +1 -0
- package/dist/utils/trace-event-builder.d.ts +122 -0
- package/dist/utils/trace-event-builder.d.ts.map +1 -0
- package/dist/utils/trace-event-builder.js +365 -0
- package/dist/utils/trace-event-builder.js.map +1 -0
- package/dist/utils/trace-file-manager.d.ts +70 -0
- package/dist/utils/trace-file-manager.d.ts.map +1 -0
- package/dist/utils/trace-file-manager.js +194 -0
- package/dist/utils/trace-file-manager.js.map +1 -0
- package/dist/utils/zod.d.ts +5 -0
- package/dist/utils/zod.d.ts.map +1 -0
- package/dist/utils/zod.js +80 -0
- package/dist/utils/zod.js.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/dist/verification.d.ts +194 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +530 -0
- package/dist/verification.js.map +1 -0
- package/dist/vision-executor.d.ts +18 -0
- package/dist/vision-executor.d.ts.map +1 -0
- package/dist/vision-executor.js +60 -0
- package/dist/vision-executor.js.map +1 -0
- package/dist/visual-agent.d.ts +120 -0
- package/dist/visual-agent.d.ts.map +1 -0
- package/dist/visual-agent.js +796 -0
- package/dist/visual-agent.js.map +1 -0
- package/dist/wait.d.ts +35 -0
- package/dist/wait.d.ts.map +1 -0
- package/dist/wait.js +76 -0
- package/dist/wait.js.map +1 -0
- package/package.json +94 -0
- package/spec/README.md +72 -0
- package/spec/SNAPSHOT_V1.md +208 -0
- package/spec/sdk-types.md +259 -0
- package/spec/snapshot.schema.json +148 -0
- package/src/extension/background.js +104 -0
- package/src/extension/content.js +162 -0
- package/src/extension/injected_api.js +1399 -0
- package/src/extension/manifest.json +36 -0
- package/src/extension/pkg/README.md +1340 -0
- package/src/extension/pkg/package.json +15 -0
- package/src/extension/pkg/sentience_core.d.ts +51 -0
- package/src/extension/pkg/sentience_core.js +371 -0
- package/src/extension/pkg/sentience_core_bg.wasm +0 -0
- package/src/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
- package/src/extension/release.json +116 -0
package/dist/actions.js
ADDED
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Actions v1 - click, type, press
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.click = click;
|
|
7
|
+
exports.typeText = typeText;
|
|
8
|
+
exports.clear = clear;
|
|
9
|
+
exports.check = check;
|
|
10
|
+
exports.uncheck = uncheck;
|
|
11
|
+
exports.selectOption = selectOption;
|
|
12
|
+
exports.uploadFile = uploadFile;
|
|
13
|
+
exports.submit = submit;
|
|
14
|
+
exports.back = back;
|
|
15
|
+
exports.scrollTo = scrollTo;
|
|
16
|
+
exports.press = press;
|
|
17
|
+
exports.sendKeys = sendKeys;
|
|
18
|
+
exports.search = search;
|
|
19
|
+
exports.clickRect = clickRect;
|
|
20
|
+
const snapshot_1 = require("./snapshot");
|
|
21
|
+
const browser_evaluator_1 = require("./utils/browser-evaluator");
|
|
22
|
+
const cursor_policy_1 = require("./cursor-policy");
|
|
23
|
+
const cursorPosByPage = new WeakMap();
|
|
24
|
+
async function humanMoveIfEnabled(page, target, cursorPolicy) {
|
|
25
|
+
if (!cursorPolicy || cursorPolicy.mode !== 'human')
|
|
26
|
+
return undefined;
|
|
27
|
+
const prev = cursorPosByPage.get(page);
|
|
28
|
+
let from;
|
|
29
|
+
if (prev) {
|
|
30
|
+
from = prev;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const vp = page.viewportSize ? page.viewportSize() : null;
|
|
34
|
+
from = vp ? { x: vp.width / 2, y: vp.height / 2 } : { x: 0, y: 0 };
|
|
35
|
+
}
|
|
36
|
+
const meta = (0, cursor_policy_1.buildHumanCursorPath)([from.x, from.y], [target.x, target.y], cursorPolicy);
|
|
37
|
+
const pts = meta.path || [];
|
|
38
|
+
const durationMs = meta.duration_ms || 0;
|
|
39
|
+
const perStepMs = durationMs > 0 ? durationMs / Math.max(1, pts.length) : 0;
|
|
40
|
+
for (const p of pts) {
|
|
41
|
+
await page.mouse.move(p.x, p.y);
|
|
42
|
+
if (perStepMs > 0)
|
|
43
|
+
await page.waitForTimeout(perStepMs);
|
|
44
|
+
}
|
|
45
|
+
if (meta.pause_before_click_ms > 0) {
|
|
46
|
+
await page.waitForTimeout(meta.pause_before_click_ms);
|
|
47
|
+
}
|
|
48
|
+
cursorPosByPage.set(page, { x: target.x, y: target.y });
|
|
49
|
+
return meta;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Highlight a rectangle with a red border overlay
|
|
53
|
+
*/
|
|
54
|
+
async function highlightRect(browser, rect, durationSec = 2.0) {
|
|
55
|
+
const page = browser.getPage();
|
|
56
|
+
if (!page) {
|
|
57
|
+
throw new Error('Browser not started. Call start() first.');
|
|
58
|
+
}
|
|
59
|
+
const highlightId = `sentience_highlight_${Date.now()}`;
|
|
60
|
+
// Combine all arguments into a single object for Playwright
|
|
61
|
+
const args = {
|
|
62
|
+
rect: {
|
|
63
|
+
x: rect.x,
|
|
64
|
+
y: rect.y,
|
|
65
|
+
w: rect.w || rect.width || 0,
|
|
66
|
+
h: rect.h || rect.height || 0,
|
|
67
|
+
},
|
|
68
|
+
highlightId,
|
|
69
|
+
durationSec,
|
|
70
|
+
};
|
|
71
|
+
await browser_evaluator_1.BrowserEvaluator.evaluate(page, (args) => {
|
|
72
|
+
const { rect, highlightId, durationSec } = args;
|
|
73
|
+
// Create overlay div
|
|
74
|
+
const overlay = document.createElement('div');
|
|
75
|
+
overlay.id = highlightId;
|
|
76
|
+
overlay.style.position = 'fixed';
|
|
77
|
+
overlay.style.left = `${rect.x}px`;
|
|
78
|
+
overlay.style.top = `${rect.y}px`;
|
|
79
|
+
overlay.style.width = `${rect.w}px`;
|
|
80
|
+
overlay.style.height = `${rect.h}px`;
|
|
81
|
+
overlay.style.border = '3px solid red';
|
|
82
|
+
overlay.style.borderRadius = '2px';
|
|
83
|
+
overlay.style.boxSizing = 'border-box';
|
|
84
|
+
overlay.style.pointerEvents = 'none';
|
|
85
|
+
overlay.style.zIndex = '999999';
|
|
86
|
+
overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
|
|
87
|
+
overlay.style.transition = 'opacity 0.3s ease-out';
|
|
88
|
+
document.body.appendChild(overlay);
|
|
89
|
+
// Remove after duration
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
overlay.style.opacity = '0';
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
if (overlay.parentNode) {
|
|
94
|
+
overlay.parentNode.removeChild(overlay);
|
|
95
|
+
}
|
|
96
|
+
}, 300); // Wait for fade-out transition
|
|
97
|
+
}, durationSec * 1000);
|
|
98
|
+
}, args);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Click an element by its ID
|
|
102
|
+
*
|
|
103
|
+
* Uses a hybrid approach: gets element bounding box from snapshot and calculates center,
|
|
104
|
+
* then uses Playwright's native mouse.click() for realistic event simulation.
|
|
105
|
+
* Falls back to JavaScript click if element not found in snapshot.
|
|
106
|
+
*
|
|
107
|
+
* @param browser - SentienceBrowser instance
|
|
108
|
+
* @param elementId - Element ID from snapshot
|
|
109
|
+
* @param useMouse - Use mouse simulation (default: true). If false, uses JavaScript click.
|
|
110
|
+
* @param takeSnapshot - Take snapshot after action (default: false)
|
|
111
|
+
* @returns ActionResult with success status, outcome, duration, and optional snapshot
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const snap = await snapshot(browser);
|
|
116
|
+
* const button = find(snap, 'role=button');
|
|
117
|
+
* if (button) {
|
|
118
|
+
* const result = await click(browser, button.id);
|
|
119
|
+
* console.log(`Click ${result.success ? 'succeeded' : 'failed'}`);
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
async function click(browser, elementId, useMouse = true, takeSnapshot = false, cursorPolicy) {
|
|
124
|
+
const page = browser.getPage();
|
|
125
|
+
if (!page) {
|
|
126
|
+
throw new Error('Browser not started. Call start() first.');
|
|
127
|
+
}
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
const urlBefore = page.url();
|
|
130
|
+
let success;
|
|
131
|
+
let cursorMeta;
|
|
132
|
+
if (useMouse) {
|
|
133
|
+
// Hybrid approach: Get element bbox from snapshot, calculate center, use mouse.click()
|
|
134
|
+
try {
|
|
135
|
+
const snap = await (0, snapshot_1.snapshot)(browser);
|
|
136
|
+
const element = snap.elements.find(el => el.id === elementId);
|
|
137
|
+
if (element) {
|
|
138
|
+
// Calculate center of element bbox
|
|
139
|
+
const centerX = element.bbox.x + element.bbox.width / 2;
|
|
140
|
+
const centerY = element.bbox.y + element.bbox.height / 2;
|
|
141
|
+
cursorMeta = await humanMoveIfEnabled(page, { x: centerX, y: centerY }, cursorPolicy);
|
|
142
|
+
// Use Playwright's native mouse click for realistic simulation
|
|
143
|
+
await page.mouse.click(centerX, centerY);
|
|
144
|
+
success = true;
|
|
145
|
+
// Keep cursor position even when not in human mode (for future moves)
|
|
146
|
+
cursorPosByPage.set(page, { x: centerX, y: centerY });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Fallback to JS click if element not found in snapshot
|
|
150
|
+
success = await browser_evaluator_1.BrowserEvaluator.evaluateWithNavigationFallback(page, id => window.sentience.click(id), elementId, true // Assume success if navigation destroyed context
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Fallback to JS click on error
|
|
156
|
+
success = await browser_evaluator_1.BrowserEvaluator.evaluateWithNavigationFallback(page, id => window.sentience.click(id), elementId, true // Assume success if navigation destroyed context
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Legacy JS-based click
|
|
162
|
+
success = await browser_evaluator_1.BrowserEvaluator.evaluateWithNavigationFallback(page, id => window.sentience.click(id), elementId, true // Assume success if navigation destroyed context
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
// Wait a bit for navigation/DOM updates
|
|
166
|
+
try {
|
|
167
|
+
await page.waitForTimeout(500);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Navigation might have happened, context destroyed
|
|
171
|
+
}
|
|
172
|
+
const durationMs = Date.now() - startTime;
|
|
173
|
+
// Check if URL changed (handle navigation gracefully)
|
|
174
|
+
let urlAfter;
|
|
175
|
+
let urlChanged;
|
|
176
|
+
try {
|
|
177
|
+
urlAfter = page.url();
|
|
178
|
+
urlChanged = urlBefore !== urlAfter;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Context destroyed due to navigation - assume URL changed
|
|
182
|
+
urlAfter = urlBefore;
|
|
183
|
+
urlChanged = true;
|
|
184
|
+
}
|
|
185
|
+
// Determine outcome
|
|
186
|
+
let outcome;
|
|
187
|
+
if (urlChanged) {
|
|
188
|
+
outcome = 'navigated';
|
|
189
|
+
}
|
|
190
|
+
else if (success) {
|
|
191
|
+
outcome = 'dom_updated';
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
outcome = 'error';
|
|
195
|
+
}
|
|
196
|
+
// Optional snapshot after
|
|
197
|
+
let snapshotAfter;
|
|
198
|
+
if (takeSnapshot) {
|
|
199
|
+
try {
|
|
200
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Navigation might have destroyed context
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
success,
|
|
208
|
+
duration_ms: durationMs,
|
|
209
|
+
outcome,
|
|
210
|
+
url_changed: urlChanged,
|
|
211
|
+
snapshot_after: snapshotAfter,
|
|
212
|
+
cursor: cursorMeta,
|
|
213
|
+
error: success
|
|
214
|
+
? undefined
|
|
215
|
+
: { code: 'click_failed', reason: 'Element not found or not clickable' },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Type text into an input element
|
|
220
|
+
*
|
|
221
|
+
* Focuses the element first, then types the text using Playwright's keyboard simulation.
|
|
222
|
+
*
|
|
223
|
+
* @param browser - SentienceBrowser instance
|
|
224
|
+
* @param elementId - Element ID from snapshot (must be a text input element)
|
|
225
|
+
* @param text - Text to type
|
|
226
|
+
* @param takeSnapshot - Take snapshot after action (default: false)
|
|
227
|
+
* @param delayMs - Delay between keystrokes in milliseconds for human-like typing (default: 0)
|
|
228
|
+
* @returns ActionResult with success status, outcome, duration, and optional snapshot
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const snap = await snapshot(browser);
|
|
233
|
+
* const searchBox = find(snap, 'role=searchbox');
|
|
234
|
+
* if (searchBox) {
|
|
235
|
+
* // Type instantly (default behavior)
|
|
236
|
+
* await typeText(browser, searchBox.id, 'Hello World');
|
|
237
|
+
*
|
|
238
|
+
* // Type with human-like delay (~10ms between keystrokes)
|
|
239
|
+
* await typeText(browser, searchBox.id, 'Hello World', false, 10);
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
async function typeText(browser, elementId, text, takeSnapshot = false, delayMs = 0) {
|
|
244
|
+
const page = browser.getPage();
|
|
245
|
+
if (!page) {
|
|
246
|
+
throw new Error('Browser not started. Call start() first.');
|
|
247
|
+
}
|
|
248
|
+
const startTime = Date.now();
|
|
249
|
+
const urlBefore = page.url();
|
|
250
|
+
// Focus element first
|
|
251
|
+
const focused = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
|
|
252
|
+
const el = window.sentience_registry[id];
|
|
253
|
+
if (el) {
|
|
254
|
+
el.focus();
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}, elementId);
|
|
259
|
+
if (!focused) {
|
|
260
|
+
return {
|
|
261
|
+
success: false,
|
|
262
|
+
duration_ms: Date.now() - startTime,
|
|
263
|
+
outcome: 'error',
|
|
264
|
+
error: { code: 'focus_failed', reason: 'Element not found' },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// Type using Playwright keyboard with optional delay between keystrokes
|
|
268
|
+
await page.keyboard.type(text, { delay: delayMs });
|
|
269
|
+
const durationMs = Date.now() - startTime;
|
|
270
|
+
const urlAfter = page.url();
|
|
271
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
272
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
273
|
+
let snapshotAfter;
|
|
274
|
+
if (takeSnapshot) {
|
|
275
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
duration_ms: durationMs,
|
|
280
|
+
outcome,
|
|
281
|
+
url_changed: urlChanged,
|
|
282
|
+
snapshot_after: snapshotAfter,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Clear the value of an input/textarea element (best-effort).
|
|
287
|
+
*/
|
|
288
|
+
async function clear(browser, elementId, takeSnapshot = false) {
|
|
289
|
+
const page = browser.getPage();
|
|
290
|
+
if (!page)
|
|
291
|
+
throw new Error('Browser not started. Call start() first.');
|
|
292
|
+
const startTime = Date.now();
|
|
293
|
+
const urlBefore = page.url();
|
|
294
|
+
const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
|
|
295
|
+
const el = window.sentience_registry?.[id];
|
|
296
|
+
if (!el)
|
|
297
|
+
return false;
|
|
298
|
+
try {
|
|
299
|
+
el.focus?.();
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
/* ignore */
|
|
303
|
+
}
|
|
304
|
+
if ('value' in el) {
|
|
305
|
+
el.value = '';
|
|
306
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
307
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}, elementId);
|
|
312
|
+
if (!ok) {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
duration_ms: Date.now() - startTime,
|
|
316
|
+
outcome: 'error',
|
|
317
|
+
error: { code: 'clear_failed', reason: 'Element not found or not clearable' },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
await page.waitForTimeout(250);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* ignore */
|
|
325
|
+
}
|
|
326
|
+
const durationMs = Date.now() - startTime;
|
|
327
|
+
const urlAfter = page.url();
|
|
328
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
329
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
330
|
+
let snapshotAfter;
|
|
331
|
+
if (takeSnapshot) {
|
|
332
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
success: true,
|
|
336
|
+
duration_ms: durationMs,
|
|
337
|
+
outcome,
|
|
338
|
+
url_changed: urlChanged,
|
|
339
|
+
snapshot_after: snapshotAfter,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Ensure a checkbox/radio is checked (best-effort).
|
|
344
|
+
*/
|
|
345
|
+
async function check(browser, elementId, takeSnapshot = false) {
|
|
346
|
+
const page = browser.getPage();
|
|
347
|
+
if (!page)
|
|
348
|
+
throw new Error('Browser not started. Call start() first.');
|
|
349
|
+
const startTime = Date.now();
|
|
350
|
+
const urlBefore = page.url();
|
|
351
|
+
const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
|
|
352
|
+
const el = window.sentience_registry?.[id];
|
|
353
|
+
if (!el)
|
|
354
|
+
return false;
|
|
355
|
+
try {
|
|
356
|
+
el.focus?.();
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* ignore */
|
|
360
|
+
}
|
|
361
|
+
if (!('checked' in el))
|
|
362
|
+
return false;
|
|
363
|
+
if (el.checked === true)
|
|
364
|
+
return true;
|
|
365
|
+
try {
|
|
366
|
+
el.click();
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}, elementId);
|
|
373
|
+
if (!ok) {
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
duration_ms: Date.now() - startTime,
|
|
377
|
+
outcome: 'error',
|
|
378
|
+
error: { code: 'check_failed', reason: 'Element not found or not checkable' },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await page.waitForTimeout(250);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
/* ignore */
|
|
386
|
+
}
|
|
387
|
+
const durationMs = Date.now() - startTime;
|
|
388
|
+
const urlAfter = page.url();
|
|
389
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
390
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
391
|
+
let snapshotAfter;
|
|
392
|
+
if (takeSnapshot)
|
|
393
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
394
|
+
return {
|
|
395
|
+
success: true,
|
|
396
|
+
duration_ms: durationMs,
|
|
397
|
+
outcome,
|
|
398
|
+
url_changed: urlChanged,
|
|
399
|
+
snapshot_after: snapshotAfter,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Ensure a checkbox/radio is unchecked (best-effort).
|
|
404
|
+
*/
|
|
405
|
+
async function uncheck(browser, elementId, takeSnapshot = false) {
|
|
406
|
+
const page = browser.getPage();
|
|
407
|
+
if (!page)
|
|
408
|
+
throw new Error('Browser not started. Call start() first.');
|
|
409
|
+
const startTime = Date.now();
|
|
410
|
+
const urlBefore = page.url();
|
|
411
|
+
const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
|
|
412
|
+
const el = window.sentience_registry?.[id];
|
|
413
|
+
if (!el)
|
|
414
|
+
return false;
|
|
415
|
+
try {
|
|
416
|
+
el.focus?.();
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
/* ignore */
|
|
420
|
+
}
|
|
421
|
+
if (!('checked' in el))
|
|
422
|
+
return false;
|
|
423
|
+
if (el.checked === false)
|
|
424
|
+
return true;
|
|
425
|
+
try {
|
|
426
|
+
el.click();
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}, elementId);
|
|
433
|
+
if (!ok) {
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
duration_ms: Date.now() - startTime,
|
|
437
|
+
outcome: 'error',
|
|
438
|
+
error: { code: 'uncheck_failed', reason: 'Element not found or not uncheckable' },
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
await page.waitForTimeout(250);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
/* ignore */
|
|
446
|
+
}
|
|
447
|
+
const durationMs = Date.now() - startTime;
|
|
448
|
+
const urlAfter = page.url();
|
|
449
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
450
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
451
|
+
let snapshotAfter;
|
|
452
|
+
if (takeSnapshot)
|
|
453
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
454
|
+
return {
|
|
455
|
+
success: true,
|
|
456
|
+
duration_ms: durationMs,
|
|
457
|
+
outcome,
|
|
458
|
+
url_changed: urlChanged,
|
|
459
|
+
snapshot_after: snapshotAfter,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Select an option in a <select> element by matching option value or label (best-effort).
|
|
464
|
+
*/
|
|
465
|
+
async function selectOption(browser, elementId, option, takeSnapshot = false) {
|
|
466
|
+
const page = browser.getPage();
|
|
467
|
+
if (!page)
|
|
468
|
+
throw new Error('Browser not started. Call start() first.');
|
|
469
|
+
const startTime = Date.now();
|
|
470
|
+
const urlBefore = page.url();
|
|
471
|
+
const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, (args) => {
|
|
472
|
+
const el = window.sentience_registry?.[args.id];
|
|
473
|
+
if (!el)
|
|
474
|
+
return false;
|
|
475
|
+
const tag = String(el.tagName || '').toUpperCase();
|
|
476
|
+
if (tag !== 'SELECT')
|
|
477
|
+
return false;
|
|
478
|
+
const needle = String(args.option ?? '');
|
|
479
|
+
const opts = Array.from(el.options || []);
|
|
480
|
+
let chosen = null;
|
|
481
|
+
for (const o of opts) {
|
|
482
|
+
const oo = o;
|
|
483
|
+
if (String(oo.value) === needle || String(oo.text) === needle) {
|
|
484
|
+
chosen = o;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!chosen) {
|
|
489
|
+
for (const o of opts) {
|
|
490
|
+
const oo = o;
|
|
491
|
+
if (String(oo.text || '').includes(needle)) {
|
|
492
|
+
chosen = o;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!chosen)
|
|
498
|
+
return false;
|
|
499
|
+
el.value = chosen.value;
|
|
500
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
501
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
502
|
+
return true;
|
|
503
|
+
}, { id: elementId, option });
|
|
504
|
+
if (!ok) {
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
duration_ms: Date.now() - startTime,
|
|
508
|
+
outcome: 'error',
|
|
509
|
+
error: { code: 'select_failed', reason: 'Element not found or option not found' },
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
await page.waitForTimeout(250);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
/* ignore */
|
|
517
|
+
}
|
|
518
|
+
const durationMs = Date.now() - startTime;
|
|
519
|
+
const urlAfter = page.url();
|
|
520
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
521
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
522
|
+
let snapshotAfter;
|
|
523
|
+
if (takeSnapshot)
|
|
524
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
525
|
+
return {
|
|
526
|
+
success: true,
|
|
527
|
+
duration_ms: durationMs,
|
|
528
|
+
outcome,
|
|
529
|
+
url_changed: urlChanged,
|
|
530
|
+
snapshot_after: snapshotAfter,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Upload a local file via an <input type="file"> element (best-effort).
|
|
535
|
+
*/
|
|
536
|
+
async function uploadFile(browser, elementId, filePath, takeSnapshot = false) {
|
|
537
|
+
const page = browser.getPage();
|
|
538
|
+
if (!page)
|
|
539
|
+
throw new Error('Browser not started. Call start() first.');
|
|
540
|
+
const startTime = Date.now();
|
|
541
|
+
const urlBefore = page.url();
|
|
542
|
+
let success = false;
|
|
543
|
+
let errorMsg;
|
|
544
|
+
try {
|
|
545
|
+
// First try: grab the exact element handle from the sentience registry.
|
|
546
|
+
try {
|
|
547
|
+
const handle = await page.evaluateHandle('(id) => (window.sentience_registry && window.sentience_registry[id]) || null', elementId);
|
|
548
|
+
const el = handle.asElement?.() ?? null;
|
|
549
|
+
if (!el)
|
|
550
|
+
throw new Error('Element not found');
|
|
551
|
+
await el.setInputFiles(filePath);
|
|
552
|
+
success = true;
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// Fallback: resolve a selector from the element's attributes and use page.setInputFiles().
|
|
556
|
+
const attrs = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
|
|
557
|
+
const el = window.sentience_registry?.[id];
|
|
558
|
+
if (!el)
|
|
559
|
+
return null;
|
|
560
|
+
const tag = String(el.tagName || '').toUpperCase();
|
|
561
|
+
const type = String(el.type || '').toLowerCase();
|
|
562
|
+
const idAttr = el.id ? String(el.id) : null;
|
|
563
|
+
const nameAttr = el.name ? String(el.name) : null;
|
|
564
|
+
return { tag, type, id: idAttr, name: nameAttr };
|
|
565
|
+
}, elementId);
|
|
566
|
+
let selector = null;
|
|
567
|
+
if (attrs && attrs.tag === 'INPUT' && attrs.type === 'file') {
|
|
568
|
+
if (attrs.id)
|
|
569
|
+
selector = `input#${attrs.id}`;
|
|
570
|
+
else if (attrs.name)
|
|
571
|
+
selector = `input[name="${String(attrs.name).replace(/"/g, '\\"')}"]`;
|
|
572
|
+
}
|
|
573
|
+
if (!selector)
|
|
574
|
+
throw new Error('Element not found');
|
|
575
|
+
await page.setInputFiles(selector, filePath);
|
|
576
|
+
success = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
success = false;
|
|
581
|
+
errorMsg = String(e?.message ?? e);
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
await page.waitForTimeout(250);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
/* ignore */
|
|
588
|
+
}
|
|
589
|
+
const durationMs = Date.now() - startTime;
|
|
590
|
+
const urlAfter = page.url();
|
|
591
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
592
|
+
const outcome = urlChanged ? 'navigated' : success ? 'dom_updated' : 'error';
|
|
593
|
+
let snapshotAfter;
|
|
594
|
+
if (takeSnapshot) {
|
|
595
|
+
try {
|
|
596
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
/* ignore */
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
success,
|
|
604
|
+
duration_ms: durationMs,
|
|
605
|
+
outcome,
|
|
606
|
+
url_changed: urlChanged,
|
|
607
|
+
snapshot_after: snapshotAfter,
|
|
608
|
+
error: success ? undefined : { code: 'upload_failed', reason: errorMsg ?? 'upload failed' },
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Submit a form (best-effort) by clicking a submit control or calling requestSubmit().
|
|
613
|
+
*/
|
|
614
|
+
async function submit(browser, elementId, takeSnapshot = false) {
|
|
615
|
+
const page = browser.getPage();
|
|
616
|
+
if (!page)
|
|
617
|
+
throw new Error('Browser not started. Call start() first.');
|
|
618
|
+
const startTime = Date.now();
|
|
619
|
+
const urlBefore = page.url();
|
|
620
|
+
const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
|
|
621
|
+
const el = window.sentience_registry?.[id];
|
|
622
|
+
if (!el)
|
|
623
|
+
return false;
|
|
624
|
+
try {
|
|
625
|
+
el.focus?.();
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
/* ignore */
|
|
629
|
+
}
|
|
630
|
+
const tag = String(el.tagName || '').toUpperCase();
|
|
631
|
+
if (tag === 'FORM') {
|
|
632
|
+
if (typeof el.requestSubmit === 'function') {
|
|
633
|
+
el.requestSubmit();
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
el.submit();
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const form = el.form;
|
|
645
|
+
if (form && typeof form.requestSubmit === 'function') {
|
|
646
|
+
form.requestSubmit();
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
el.click();
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
}, elementId);
|
|
657
|
+
if (!ok) {
|
|
658
|
+
return {
|
|
659
|
+
success: false,
|
|
660
|
+
duration_ms: Date.now() - startTime,
|
|
661
|
+
outcome: 'error',
|
|
662
|
+
error: { code: 'submit_failed', reason: 'Element not found or not submittable' },
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await page.waitForTimeout(500);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
/* ignore */
|
|
670
|
+
}
|
|
671
|
+
const durationMs = Date.now() - startTime;
|
|
672
|
+
const urlAfter = page.url();
|
|
673
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
674
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
675
|
+
let snapshotAfter;
|
|
676
|
+
if (takeSnapshot) {
|
|
677
|
+
try {
|
|
678
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
/* ignore */
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
duration_ms: durationMs,
|
|
687
|
+
outcome,
|
|
688
|
+
url_changed: urlChanged,
|
|
689
|
+
snapshot_after: snapshotAfter,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Navigate back in history (best-effort).
|
|
694
|
+
*/
|
|
695
|
+
async function back(browser, takeSnapshot = false) {
|
|
696
|
+
const page = browser.getPage();
|
|
697
|
+
if (!page)
|
|
698
|
+
throw new Error('Browser not started. Call start() first.');
|
|
699
|
+
const startTime = Date.now();
|
|
700
|
+
const urlBefore = page.url();
|
|
701
|
+
let success = false;
|
|
702
|
+
let errorMsg;
|
|
703
|
+
try {
|
|
704
|
+
await page.goBack();
|
|
705
|
+
success = true;
|
|
706
|
+
}
|
|
707
|
+
catch (e) {
|
|
708
|
+
success = false;
|
|
709
|
+
errorMsg = String(e?.message ?? e);
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
await page.waitForTimeout(500);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
/* ignore */
|
|
716
|
+
}
|
|
717
|
+
const durationMs = Date.now() - startTime;
|
|
718
|
+
let urlChanged = false;
|
|
719
|
+
try {
|
|
720
|
+
urlChanged = urlBefore !== page.url();
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
urlChanged = true;
|
|
724
|
+
}
|
|
725
|
+
const outcome = urlChanged ? 'navigated' : success ? 'dom_updated' : 'error';
|
|
726
|
+
let snapshotAfter;
|
|
727
|
+
if (takeSnapshot) {
|
|
728
|
+
try {
|
|
729
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
/* ignore */
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
success,
|
|
737
|
+
duration_ms: durationMs,
|
|
738
|
+
outcome,
|
|
739
|
+
url_changed: urlChanged,
|
|
740
|
+
snapshot_after: snapshotAfter,
|
|
741
|
+
error: success ? undefined : { code: 'back_failed', reason: errorMsg ?? 'back failed' },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Scroll an element into view
|
|
746
|
+
*
|
|
747
|
+
* Scrolls the page so that the specified element is visible in the viewport.
|
|
748
|
+
* Uses the element registry to find the element and scrollIntoView() to scroll it.
|
|
749
|
+
*
|
|
750
|
+
* @param browser - SentienceBrowser instance
|
|
751
|
+
* @param elementId - Element ID from snapshot to scroll into view
|
|
752
|
+
* @param behavior - Scroll behavior: 'smooth' for animated scroll, 'instant' for immediate (default: 'smooth')
|
|
753
|
+
* @param block - Vertical alignment: 'start', 'center', 'end', 'nearest' (default: 'center')
|
|
754
|
+
* @param takeSnapshot - Take snapshot after action (default: false)
|
|
755
|
+
* @returns ActionResult with success status, outcome, duration, and optional snapshot
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```typescript
|
|
759
|
+
* const snap = await snapshot(browser);
|
|
760
|
+
* const button = find(snap, 'role=button[name="Submit"]');
|
|
761
|
+
* if (button) {
|
|
762
|
+
* // Scroll element into view with smooth animation
|
|
763
|
+
* await scrollTo(browser, button.id);
|
|
764
|
+
*
|
|
765
|
+
* // Scroll instantly to top of viewport
|
|
766
|
+
* await scrollTo(browser, button.id, 'instant', 'start');
|
|
767
|
+
* }
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
async function scrollTo(browser, elementId, behavior = 'smooth', block = 'center', takeSnapshot = false) {
|
|
771
|
+
const page = browser.getPage();
|
|
772
|
+
if (!page) {
|
|
773
|
+
throw new Error('Browser not started. Call start() first.');
|
|
774
|
+
}
|
|
775
|
+
const startTime = Date.now();
|
|
776
|
+
const urlBefore = page.url();
|
|
777
|
+
// Scroll element into view using the element registry
|
|
778
|
+
const scrolled = await browser_evaluator_1.BrowserEvaluator.evaluate(page, (args) => {
|
|
779
|
+
const el = window.sentience_registry[args.id];
|
|
780
|
+
if (el && el.scrollIntoView) {
|
|
781
|
+
el.scrollIntoView({
|
|
782
|
+
behavior: args.behavior,
|
|
783
|
+
block: args.block,
|
|
784
|
+
inline: 'nearest',
|
|
785
|
+
});
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}, { id: elementId, behavior, block });
|
|
790
|
+
if (!scrolled) {
|
|
791
|
+
return {
|
|
792
|
+
success: false,
|
|
793
|
+
duration_ms: Date.now() - startTime,
|
|
794
|
+
outcome: 'error',
|
|
795
|
+
error: { code: 'scroll_failed', reason: 'Element not found or not scrollable' },
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// Wait a bit for scroll to complete (especially for smooth scrolling)
|
|
799
|
+
await page.waitForTimeout(behavior === 'smooth' ? 500 : 100);
|
|
800
|
+
const durationMs = Date.now() - startTime;
|
|
801
|
+
const urlAfter = page.url();
|
|
802
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
803
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
804
|
+
let snapshotAfter;
|
|
805
|
+
if (takeSnapshot) {
|
|
806
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
success: true,
|
|
810
|
+
duration_ms: durationMs,
|
|
811
|
+
outcome,
|
|
812
|
+
url_changed: urlChanged,
|
|
813
|
+
snapshot_after: snapshotAfter,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Press a keyboard key
|
|
818
|
+
*
|
|
819
|
+
* Simulates pressing a key using Playwright's keyboard API.
|
|
820
|
+
* Common keys: 'Enter', 'Escape', 'Tab', 'ArrowUp', 'ArrowDown', etc.
|
|
821
|
+
*
|
|
822
|
+
* @param browser - SentienceBrowser instance
|
|
823
|
+
* @param key - Key to press (e.g., 'Enter', 'Escape', 'Tab')
|
|
824
|
+
* @param takeSnapshot - Take snapshot after action (default: false)
|
|
825
|
+
* @returns ActionResult with success status, outcome, duration, and optional snapshot
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```typescript
|
|
829
|
+
* // Press Enter after typing
|
|
830
|
+
* await typeText(browser, elementId, 'search query');
|
|
831
|
+
* await press(browser, 'Enter');
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
async function press(browser, key, takeSnapshot = false) {
|
|
835
|
+
const page = browser.getPage();
|
|
836
|
+
if (!page) {
|
|
837
|
+
throw new Error('Browser not started. Call start() first.');
|
|
838
|
+
}
|
|
839
|
+
const startTime = Date.now();
|
|
840
|
+
const urlBefore = page.url();
|
|
841
|
+
// Press key using Playwright
|
|
842
|
+
await page.keyboard.press(key);
|
|
843
|
+
// Wait a bit for navigation/DOM updates
|
|
844
|
+
await page.waitForTimeout(500);
|
|
845
|
+
const durationMs = Date.now() - startTime;
|
|
846
|
+
const urlAfter = page.url();
|
|
847
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
848
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
849
|
+
let snapshotAfter;
|
|
850
|
+
if (takeSnapshot) {
|
|
851
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
success: true,
|
|
855
|
+
duration_ms: durationMs,
|
|
856
|
+
outcome,
|
|
857
|
+
url_changed: urlChanged,
|
|
858
|
+
snapshot_after: snapshotAfter,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function normalizeKeyToken(token) {
|
|
862
|
+
const lookup = {
|
|
863
|
+
CMD: 'Meta',
|
|
864
|
+
COMMAND: 'Meta',
|
|
865
|
+
CTRL: 'Control',
|
|
866
|
+
CONTROL: 'Control',
|
|
867
|
+
ALT: 'Alt',
|
|
868
|
+
OPTION: 'Alt',
|
|
869
|
+
SHIFT: 'Shift',
|
|
870
|
+
ESC: 'Escape',
|
|
871
|
+
ESCAPE: 'Escape',
|
|
872
|
+
ENTER: 'Enter',
|
|
873
|
+
RETURN: 'Enter',
|
|
874
|
+
TAB: 'Tab',
|
|
875
|
+
SPACE: 'Space',
|
|
876
|
+
};
|
|
877
|
+
const upper = token.trim().toUpperCase();
|
|
878
|
+
return lookup[upper] ?? token.trim();
|
|
879
|
+
}
|
|
880
|
+
function parseKeySequence(sequence) {
|
|
881
|
+
const parts = [];
|
|
882
|
+
for (const rawPart of sequence.replace(/,/g, ' ').split(/\s+/)) {
|
|
883
|
+
let raw = rawPart.trim();
|
|
884
|
+
if (!raw)
|
|
885
|
+
continue;
|
|
886
|
+
if (raw.startsWith('{') && raw.endsWith('}')) {
|
|
887
|
+
raw = raw.slice(1, -1);
|
|
888
|
+
}
|
|
889
|
+
if (raw.includes('+')) {
|
|
890
|
+
const combo = raw
|
|
891
|
+
.split('+')
|
|
892
|
+
.filter(Boolean)
|
|
893
|
+
.map(token => normalizeKeyToken(token))
|
|
894
|
+
.join('+');
|
|
895
|
+
parts.push(combo);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
parts.push(normalizeKeyToken(raw));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return parts;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Send a sequence of key presses (e.g., "CMD+H", "CTRL+SHIFT+P").
|
|
905
|
+
*/
|
|
906
|
+
async function sendKeys(browser, sequence, takeSnapshot = false, delayMs = 50) {
|
|
907
|
+
const page = browser.getPage();
|
|
908
|
+
if (!page) {
|
|
909
|
+
throw new Error('Browser not started. Call start() first.');
|
|
910
|
+
}
|
|
911
|
+
const startTime = Date.now();
|
|
912
|
+
const urlBefore = page.url();
|
|
913
|
+
const keys = parseKeySequence(sequence);
|
|
914
|
+
if (keys.length === 0) {
|
|
915
|
+
throw new Error('send_keys sequence is empty');
|
|
916
|
+
}
|
|
917
|
+
for (const key of keys) {
|
|
918
|
+
await page.keyboard.press(key);
|
|
919
|
+
if (delayMs > 0) {
|
|
920
|
+
await page.waitForTimeout(delayMs);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const durationMs = Date.now() - startTime;
|
|
924
|
+
const urlAfter = page.url();
|
|
925
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
926
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
927
|
+
let snapshotAfter;
|
|
928
|
+
if (takeSnapshot) {
|
|
929
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
success: true,
|
|
933
|
+
duration_ms: durationMs,
|
|
934
|
+
outcome,
|
|
935
|
+
url_changed: urlChanged,
|
|
936
|
+
snapshot_after: snapshotAfter,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function buildSearchUrl(query, engine) {
|
|
940
|
+
const q = encodeURIComponent(query).replace(/%20/g, '+');
|
|
941
|
+
const key = engine.trim().toLowerCase();
|
|
942
|
+
if (key === 'duckduckgo' || key === 'ddg') {
|
|
943
|
+
return `https://duckduckgo.com/?q=${q}`;
|
|
944
|
+
}
|
|
945
|
+
if (key === 'google.com' || key === 'google') {
|
|
946
|
+
return `https://www.google.com/search?q=${q}`;
|
|
947
|
+
}
|
|
948
|
+
if (key === 'bing') {
|
|
949
|
+
return `https://www.bing.com/search?q=${q}`;
|
|
950
|
+
}
|
|
951
|
+
throw new Error(`unsupported search engine: ${engine}`);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Navigate to a search results page for the given query.
|
|
955
|
+
*/
|
|
956
|
+
async function search(browser, query, engine = 'duckduckgo', takeSnapshot = false, snapshotOptions = undefined) {
|
|
957
|
+
const page = browser.getPage();
|
|
958
|
+
if (!page) {
|
|
959
|
+
throw new Error('Browser not started. Call start() first.');
|
|
960
|
+
}
|
|
961
|
+
if (!query.trim()) {
|
|
962
|
+
throw new Error('search query is empty');
|
|
963
|
+
}
|
|
964
|
+
const startTime = Date.now();
|
|
965
|
+
const urlBefore = page.url();
|
|
966
|
+
const url = buildSearchUrl(query, engine);
|
|
967
|
+
await browser.goto(url);
|
|
968
|
+
// Use lightweight readiness checks instead of networkidle.
|
|
969
|
+
// On Windows CI and some search engines, networkidle can remain pending due to
|
|
970
|
+
// long-lived background requests and cause flaky timeouts.
|
|
971
|
+
try {
|
|
972
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
973
|
+
await page.waitForLoadState('load', { timeout: 5000 });
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
// best-effort only; URL/outcome assertions do not require full idle
|
|
977
|
+
}
|
|
978
|
+
const durationMs = Date.now() - startTime;
|
|
979
|
+
const urlAfter = page.url();
|
|
980
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
981
|
+
const outcome = urlChanged ? 'navigated' : 'dom_updated';
|
|
982
|
+
let snapshotAfter;
|
|
983
|
+
if (takeSnapshot) {
|
|
984
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser, snapshotOptions);
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
success: true,
|
|
988
|
+
duration_ms: durationMs,
|
|
989
|
+
outcome,
|
|
990
|
+
url_changed: urlChanged,
|
|
991
|
+
snapshot_after: snapshotAfter,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Click at the center of a rectangle using Playwright's native mouse simulation.
|
|
996
|
+
* This uses a hybrid approach: calculates center coordinates and uses mouse.click()
|
|
997
|
+
* for realistic event simulation (triggers hover, focus, mousedown, mouseup).
|
|
998
|
+
*
|
|
999
|
+
* @param browser - SentienceBrowser instance
|
|
1000
|
+
* @param rect - Rectangle with x, y, w (or width), h (or height) keys, or BBox object
|
|
1001
|
+
* @param highlight - Whether to show a red border highlight when clicking (default: true)
|
|
1002
|
+
* @param highlightDuration - How long to show the highlight in seconds (default: 2.0)
|
|
1003
|
+
* @param takeSnapshot - Whether to take snapshot after action
|
|
1004
|
+
* @returns ActionResult
|
|
1005
|
+
*
|
|
1006
|
+
* @example
|
|
1007
|
+
* ```typescript
|
|
1008
|
+
* // Click using rect object
|
|
1009
|
+
* await clickRect(browser, { x: 100, y: 200, w: 50, h: 30 });
|
|
1010
|
+
*
|
|
1011
|
+
* // Click using BBox from element
|
|
1012
|
+
* const snap = await snapshot(browser);
|
|
1013
|
+
* const element = find(snap, "role=button");
|
|
1014
|
+
* if (element) {
|
|
1015
|
+
* await clickRect(browser, {
|
|
1016
|
+
* x: element.bbox.x,
|
|
1017
|
+
* y: element.bbox.y,
|
|
1018
|
+
* w: element.bbox.width,
|
|
1019
|
+
* h: element.bbox.height
|
|
1020
|
+
* });
|
|
1021
|
+
* }
|
|
1022
|
+
*
|
|
1023
|
+
* // Without highlight
|
|
1024
|
+
* await clickRect(browser, { x: 100, y: 200, w: 50, h: 30 }, false);
|
|
1025
|
+
*
|
|
1026
|
+
* // Custom highlight duration
|
|
1027
|
+
* await clickRect(browser, { x: 100, y: 200, w: 50, h: 30 }, true, 3.0);
|
|
1028
|
+
* ```
|
|
1029
|
+
*/
|
|
1030
|
+
async function clickRect(browser, rect, highlight = true, highlightDuration = 2.0, takeSnapshot = false, cursorPolicy) {
|
|
1031
|
+
const page = browser.getPage();
|
|
1032
|
+
if (!page) {
|
|
1033
|
+
throw new Error('Browser not started. Call start() first.');
|
|
1034
|
+
}
|
|
1035
|
+
const startTime = Date.now();
|
|
1036
|
+
const urlBefore = page.url();
|
|
1037
|
+
// Handle BBox object or ClickRect dict
|
|
1038
|
+
let x, y, w, h;
|
|
1039
|
+
if ('width' in rect && 'height' in rect && !('w' in rect) && !('h' in rect)) {
|
|
1040
|
+
// BBox object (width and height are required in BBox)
|
|
1041
|
+
const bbox = rect;
|
|
1042
|
+
x = bbox.x;
|
|
1043
|
+
y = bbox.y;
|
|
1044
|
+
w = bbox.width;
|
|
1045
|
+
h = bbox.height;
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
// ClickRect dict
|
|
1049
|
+
const clickRect = rect;
|
|
1050
|
+
x = clickRect.x;
|
|
1051
|
+
y = clickRect.y;
|
|
1052
|
+
w = clickRect.w || clickRect.width || 0;
|
|
1053
|
+
h = clickRect.h || clickRect.height || 0;
|
|
1054
|
+
}
|
|
1055
|
+
if (w <= 0 || h <= 0) {
|
|
1056
|
+
return {
|
|
1057
|
+
success: false,
|
|
1058
|
+
duration_ms: 0,
|
|
1059
|
+
outcome: 'error',
|
|
1060
|
+
error: {
|
|
1061
|
+
code: 'invalid_rect',
|
|
1062
|
+
reason: 'Rectangle width and height must be positive',
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
// Calculate center of rectangle
|
|
1067
|
+
const centerX = x + w / 2;
|
|
1068
|
+
const centerY = y + h / 2;
|
|
1069
|
+
let cursorMeta;
|
|
1070
|
+
// Show highlight before clicking (if enabled)
|
|
1071
|
+
if (highlight) {
|
|
1072
|
+
await highlightRect(browser, { x, y, w, h }, highlightDuration);
|
|
1073
|
+
// Small delay to ensure highlight is visible
|
|
1074
|
+
await page.waitForTimeout(50);
|
|
1075
|
+
}
|
|
1076
|
+
// Use Playwright's native mouse click for realistic simulation
|
|
1077
|
+
let success;
|
|
1078
|
+
let errorMsg;
|
|
1079
|
+
try {
|
|
1080
|
+
cursorMeta = await humanMoveIfEnabled(page, { x: centerX, y: centerY }, cursorPolicy);
|
|
1081
|
+
await page.mouse.click(centerX, centerY);
|
|
1082
|
+
success = true;
|
|
1083
|
+
cursorPosByPage.set(page, { x: centerX, y: centerY });
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
success = false;
|
|
1087
|
+
errorMsg = error instanceof Error ? error.message : String(error);
|
|
1088
|
+
}
|
|
1089
|
+
// Wait a bit for navigation/DOM updates
|
|
1090
|
+
await page.waitForTimeout(500);
|
|
1091
|
+
const durationMs = Date.now() - startTime;
|
|
1092
|
+
const urlAfter = page.url();
|
|
1093
|
+
const urlChanged = urlBefore !== urlAfter;
|
|
1094
|
+
// Determine outcome
|
|
1095
|
+
let outcome;
|
|
1096
|
+
if (urlChanged) {
|
|
1097
|
+
outcome = 'navigated';
|
|
1098
|
+
}
|
|
1099
|
+
else if (success) {
|
|
1100
|
+
outcome = 'dom_updated';
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
outcome = 'error';
|
|
1104
|
+
}
|
|
1105
|
+
// Optional snapshot after
|
|
1106
|
+
let snapshotAfter;
|
|
1107
|
+
if (takeSnapshot) {
|
|
1108
|
+
snapshotAfter = await (0, snapshot_1.snapshot)(browser);
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
success,
|
|
1112
|
+
duration_ms: durationMs,
|
|
1113
|
+
outcome,
|
|
1114
|
+
url_changed: urlChanged,
|
|
1115
|
+
snapshot_after: snapshotAfter,
|
|
1116
|
+
cursor: cursorMeta,
|
|
1117
|
+
error: success ? undefined : { code: 'click_failed', reason: errorMsg || 'Click failed' },
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
//# sourceMappingURL=actions.js.map
|