arcy.js 0.1.0 → 0.1.2
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/arcy.chat.39921845e233.js +1271 -0
- package/dist/arcy.legacy.7037dec7a67f.js +568 -0
- package/dist/arcy.loader.js +1 -1
- package/dist/arcy.modern.0ffea3108a8d.js +568 -0
- package/dist/arcy.picker.73f47221b73c.js +59 -0
- package/dist/chat-IU4IJMGO.js +3860 -0
- package/dist/chunk-7VVKR3AO.js +122 -0
- package/dist/chunk-CQ2DESAJ.js +263 -0
- package/dist/chunk-DKYMIAYF.js +7 -0
- package/dist/chunk-DW3GUQ6J.js +234 -0
- package/dist/chunk-FR6SJSDU.js +220 -0
- package/dist/chunk-LGWYJSYX.js +271 -0
- package/dist/chunk-NIHUDSWK.js +155 -0
- package/dist/chunk-NY3NXM2V.js +213 -0
- package/dist/design-mode.js +2 -127
- package/dist/flow-M7W3H3C5.js +1422 -0
- package/dist/index.cjs +9915 -1158
- package/dist/index.d.ts +44 -8
- package/dist/index.js +799 -1157
- package/dist/picker-JPESE6JJ.js +2134 -0
- package/package.json +1 -1
- package/dist/arcy.chat.ab181cf5a84b.js +0 -1258
- package/dist/arcy.legacy.ab4e9266b657.js +0 -567
- package/dist/arcy.modern.60faeffc62a2.js +0 -567
- package/dist/arcy.picker.c598a581d964.js +0 -59
- package/dist/design-mode.d.cts +0 -197
- package/dist/index.d.cts +0 -133
- package/dist/snippet.d.cts +0 -39
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
import { FLOW_CONTRACT, FLOW_GLOBAL, createFlowRunState, createPreviewRunState, uuidV4 } from './chunk-FR6SJSDU.js';
|
|
2
|
+
import { readFlowDetail, normalizeOptionReply, validateFillReply, fetchFlowRun } from './chunk-CQ2DESAJ.js';
|
|
3
|
+
import { matchTarget } from './chunk-NIHUDSWK.js';
|
|
4
|
+
import { collectElementsDeep, isInteractive, captureEventFingerprint } from './chunk-NY3NXM2V.js';
|
|
5
|
+
|
|
6
|
+
/* arcy.js — https://arcyai.com */
|
|
7
|
+
|
|
8
|
+
// src/fingerprint/manifest.ts
|
|
9
|
+
var LIVE_MANIFEST_MAX_ELEMENTS = 40;
|
|
10
|
+
function captureLiveManifest(doc = document) {
|
|
11
|
+
try {
|
|
12
|
+
const candidates = collectElementsDeep(doc);
|
|
13
|
+
const manifest = [];
|
|
14
|
+
for (const el of candidates) {
|
|
15
|
+
if (manifest.length >= LIVE_MANIFEST_MAX_ELEMENTS) break;
|
|
16
|
+
if (!isInteractive(el)) continue;
|
|
17
|
+
manifest.push(captureEventFingerprint(el));
|
|
18
|
+
}
|
|
19
|
+
return manifest;
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/flow/condition.ts
|
|
26
|
+
var MAX_CONDITION_DEPTH = 3;
|
|
27
|
+
var CONDITION_TYPES = [
|
|
28
|
+
"attribute",
|
|
29
|
+
"current_page",
|
|
30
|
+
"element",
|
|
31
|
+
"text_input",
|
|
32
|
+
"user_fills_input",
|
|
33
|
+
"current_time",
|
|
34
|
+
"user_idle",
|
|
35
|
+
"visitor",
|
|
36
|
+
"always_true",
|
|
37
|
+
"group"
|
|
38
|
+
];
|
|
39
|
+
function isTargetFingerprint(value) {
|
|
40
|
+
if (!value || typeof value !== "object") return false;
|
|
41
|
+
const v = value;
|
|
42
|
+
return v.v === 1 && !!v.core && typeof v.core === "object" && typeof v.core.tag === "string" && (v.selector === null || typeof v.selector === "string") && typeof v.ordinal === "number" && typeof v.precision === "number";
|
|
43
|
+
}
|
|
44
|
+
function parseNode(value, depth) {
|
|
45
|
+
if (depth > MAX_CONDITION_DEPTH) return null;
|
|
46
|
+
if (!value || typeof value !== "object") return null;
|
|
47
|
+
const v = value;
|
|
48
|
+
const type = v.type;
|
|
49
|
+
if (typeof type !== "string" || !CONDITION_TYPES.includes(type)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
switch (type) {
|
|
53
|
+
case "attribute": {
|
|
54
|
+
if (typeof v.codeName !== "string" || typeof v.operator !== "string") return null;
|
|
55
|
+
return {
|
|
56
|
+
type: "attribute",
|
|
57
|
+
codeName: v.codeName,
|
|
58
|
+
operator: v.operator,
|
|
59
|
+
value: v.value
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case "current_page": {
|
|
63
|
+
if (!Array.isArray(v.matches) || !Array.isArray(v.doesNotMatch)) return null;
|
|
64
|
+
return {
|
|
65
|
+
type: "current_page",
|
|
66
|
+
matches: v.matches.filter((m) => typeof m === "string"),
|
|
67
|
+
doesNotMatch: v.doesNotMatch.filter(
|
|
68
|
+
(m) => typeof m === "string"
|
|
69
|
+
)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case "element": {
|
|
73
|
+
if (!isTargetFingerprint(v.target) || typeof v.state !== "string") return null;
|
|
74
|
+
return {
|
|
75
|
+
type: "element",
|
|
76
|
+
target: v.target,
|
|
77
|
+
state: v.state
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "text_input": {
|
|
81
|
+
if (!isTargetFingerprint(v.target) || typeof v.operator !== "string" || typeof v.value !== "string") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
type: "text_input",
|
|
86
|
+
target: v.target,
|
|
87
|
+
operator: v.operator,
|
|
88
|
+
value: v.value
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case "user_fills_input": {
|
|
92
|
+
if (!isTargetFingerprint(v.target)) return null;
|
|
93
|
+
return { type: "user_fills_input", target: v.target };
|
|
94
|
+
}
|
|
95
|
+
case "current_time": {
|
|
96
|
+
const from = v.from;
|
|
97
|
+
const to = v.to;
|
|
98
|
+
if (from !== null && typeof from !== "string" || to !== null && typeof to !== "string") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return { type: "current_time", from: from ?? null, to: to ?? null };
|
|
102
|
+
}
|
|
103
|
+
case "user_idle": {
|
|
104
|
+
const seconds = v.seconds;
|
|
105
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 1 || seconds > 600) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return { type: "user_idle", seconds };
|
|
109
|
+
}
|
|
110
|
+
case "visitor": {
|
|
111
|
+
if (v.state !== "first_visit" && v.state !== "returning") return null;
|
|
112
|
+
return { type: "visitor", state: v.state };
|
|
113
|
+
}
|
|
114
|
+
case "always_true":
|
|
115
|
+
return { type: "always_true" };
|
|
116
|
+
case "group": {
|
|
117
|
+
if (!Array.isArray(v.children) || v.operator !== "and" && v.operator !== "or") {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const children = [];
|
|
121
|
+
for (const child of v.children) {
|
|
122
|
+
const parsed = parseNode(child, depth + 1);
|
|
123
|
+
if (parsed) children.push(parsed);
|
|
124
|
+
}
|
|
125
|
+
return { type: "group", operator: v.operator, children };
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function parseTriggerCondition(value) {
|
|
132
|
+
if (!value || typeof value !== "object") return null;
|
|
133
|
+
const v = value;
|
|
134
|
+
if (v.v !== 1) return null;
|
|
135
|
+
const node = parseNode(v.node, 0);
|
|
136
|
+
return node ? { v: 1, node } : null;
|
|
137
|
+
}
|
|
138
|
+
function parseHHMM(value) {
|
|
139
|
+
const match = /^(\d{2}):(\d{2})$/.exec(value);
|
|
140
|
+
if (!match) return null;
|
|
141
|
+
const hours = Number(match[1]);
|
|
142
|
+
const minutes = Number(match[2]);
|
|
143
|
+
if (hours > 23 || minutes > 59) return null;
|
|
144
|
+
return hours * 60 + minutes;
|
|
145
|
+
}
|
|
146
|
+
function evaluateCurrentTime(node, now) {
|
|
147
|
+
if (node.from === null && node.to === null) return true;
|
|
148
|
+
const nowMinutes = now.getHours() * 60 + now.getMinutes();
|
|
149
|
+
const from = node.from !== null ? parseHHMM(node.from) : null;
|
|
150
|
+
const to = node.to !== null ? parseHHMM(node.to) : null;
|
|
151
|
+
if (node.from !== null && from === null) return false;
|
|
152
|
+
if (node.to !== null && to === null) return false;
|
|
153
|
+
if (from !== null && to === null) return nowMinutes >= from;
|
|
154
|
+
if (from === null && to !== null) return nowMinutes <= to;
|
|
155
|
+
if (from !== null && to !== null) {
|
|
156
|
+
if (from <= to) return nowMinutes >= from && nowMinutes <= to;
|
|
157
|
+
return nowMinutes >= from || nowMinutes <= to;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
function globMatches(pattern, value) {
|
|
162
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
163
|
+
try {
|
|
164
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function evaluateCurrentPage(node, href) {
|
|
170
|
+
if (node.doesNotMatch.some((pattern) => globMatches(pattern, href))) return false;
|
|
171
|
+
if (node.matches.length === 0) return true;
|
|
172
|
+
return node.matches.some((pattern) => globMatches(pattern, href));
|
|
173
|
+
}
|
|
174
|
+
function compareText(operator, live, expected) {
|
|
175
|
+
if (live === void 0) return false;
|
|
176
|
+
switch (operator) {
|
|
177
|
+
case "is":
|
|
178
|
+
return live === expected;
|
|
179
|
+
case "is_not":
|
|
180
|
+
return live !== expected;
|
|
181
|
+
case "contains":
|
|
182
|
+
return live.includes(expected);
|
|
183
|
+
case "does_not_contain":
|
|
184
|
+
return !live.includes(expected);
|
|
185
|
+
case "starts_with":
|
|
186
|
+
return live.startsWith(expected);
|
|
187
|
+
case "ends_with":
|
|
188
|
+
return live.endsWith(expected);
|
|
189
|
+
// D1297's catalogue fill-out. The empty pair reads the LIVE value only,
|
|
190
|
+
// so an authored `value` is ignored rather than misread.
|
|
191
|
+
case "is_empty":
|
|
192
|
+
return live === "";
|
|
193
|
+
case "is_not_empty":
|
|
194
|
+
return live !== "";
|
|
195
|
+
// Numeric compares parse both sides; either side not a number means the
|
|
196
|
+
// condition cannot hold (never throw, never coerce NaN into an answer).
|
|
197
|
+
case "greater_than":
|
|
198
|
+
case "less_than": {
|
|
199
|
+
const liveNumber = Number(live);
|
|
200
|
+
const expectedNumber = Number(expected);
|
|
201
|
+
if (live.trim() === "" || !Number.isFinite(liveNumber) || !Number.isFinite(expectedNumber)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return operator === "greater_than" ? liveNumber > expectedNumber : liveNumber < expectedNumber;
|
|
205
|
+
}
|
|
206
|
+
// The same `*` glob `current_page` uses, applied to a field value.
|
|
207
|
+
case "matches_pattern":
|
|
208
|
+
return globMatches(expected, live);
|
|
209
|
+
default:
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function isElementVisible(el, doc) {
|
|
214
|
+
try {
|
|
215
|
+
if (el.getClientRects().length === 0) return false;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const style = doc.defaultView?.getComputedStyle(el);
|
|
221
|
+
if (style && style.visibility === "hidden") return false;
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function isElementFocused(el, doc) {
|
|
227
|
+
try {
|
|
228
|
+
let active = doc.activeElement;
|
|
229
|
+
while (active) {
|
|
230
|
+
if (active === el || el.contains(active)) return true;
|
|
231
|
+
const root = active.shadowRoot;
|
|
232
|
+
active = root?.activeElement ?? null;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function evaluateElement(node, ctx) {
|
|
239
|
+
const result = matchTarget(node.target, ctx.doc);
|
|
240
|
+
switch (node.state) {
|
|
241
|
+
case "is_present":
|
|
242
|
+
return result.status === "matched";
|
|
243
|
+
case "is_not_present":
|
|
244
|
+
return result.status === "not_found";
|
|
245
|
+
case "is_disabled":
|
|
246
|
+
case "is_not_disabled": {
|
|
247
|
+
if (result.status !== "matched") return false;
|
|
248
|
+
const disabled = "disabled" in result.element && result.element.disabled === true;
|
|
249
|
+
return node.state === "is_disabled" ? disabled : !disabled;
|
|
250
|
+
}
|
|
251
|
+
case "is_clicked":
|
|
252
|
+
return ctx.wasClicked(node.target);
|
|
253
|
+
case "is_not_clicked":
|
|
254
|
+
return !ctx.wasClicked(node.target);
|
|
255
|
+
case "is_visible":
|
|
256
|
+
return result.status === "matched" && isElementVisible(result.element, ctx.doc);
|
|
257
|
+
case "is_not_visible":
|
|
258
|
+
return result.status !== "matched" || !isElementVisible(result.element, ctx.doc);
|
|
259
|
+
case "is_checked":
|
|
260
|
+
case "is_not_checked": {
|
|
261
|
+
if (result.status !== "matched") return false;
|
|
262
|
+
const checked = "checked" in result.element && result.element.checked === true;
|
|
263
|
+
return node.state === "is_checked" ? checked : !checked;
|
|
264
|
+
}
|
|
265
|
+
case "is_focused":
|
|
266
|
+
return result.status === "matched" && isElementFocused(result.element, ctx.doc);
|
|
267
|
+
case "is_not_focused":
|
|
268
|
+
return result.status !== "matched" || !isElementFocused(result.element, ctx.doc);
|
|
269
|
+
default:
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function evaluateAttribute(node, ctx) {
|
|
274
|
+
const live = ctx.getTrait?.(node.codeName);
|
|
275
|
+
switch (node.operator) {
|
|
276
|
+
case "has_any_value":
|
|
277
|
+
return live !== void 0 && live !== null && live !== "";
|
|
278
|
+
case "is_empty":
|
|
279
|
+
return live === void 0 || live === null || live === "";
|
|
280
|
+
}
|
|
281
|
+
if (live === void 0) return false;
|
|
282
|
+
const liveText = String(live);
|
|
283
|
+
const expected = node.value === void 0 ? "" : String(node.value);
|
|
284
|
+
return compareText(node.operator, liveText, expected);
|
|
285
|
+
}
|
|
286
|
+
function evaluateCondition(node, ctx) {
|
|
287
|
+
switch (node.type) {
|
|
288
|
+
case "always_true":
|
|
289
|
+
return true;
|
|
290
|
+
case "current_time":
|
|
291
|
+
return evaluateCurrentTime(node, ctx.now());
|
|
292
|
+
case "current_page":
|
|
293
|
+
return evaluateCurrentPage(node, ctx.href());
|
|
294
|
+
case "element":
|
|
295
|
+
return evaluateElement(node, ctx);
|
|
296
|
+
case "attribute":
|
|
297
|
+
return evaluateAttribute(node, ctx);
|
|
298
|
+
case "text_input": {
|
|
299
|
+
const live = ctx.readInputValue(node.target);
|
|
300
|
+
return compareText(node.operator, live, node.value);
|
|
301
|
+
}
|
|
302
|
+
case "user_fills_input": {
|
|
303
|
+
const live = ctx.readInputValue(node.target);
|
|
304
|
+
const initial = ctx.initialInputValue(node.target);
|
|
305
|
+
return live !== void 0 && live !== "" && live !== initial;
|
|
306
|
+
}
|
|
307
|
+
case "user_idle": {
|
|
308
|
+
const idle = ctx.idleSeconds?.();
|
|
309
|
+
return idle !== void 0 && idle >= node.seconds;
|
|
310
|
+
}
|
|
311
|
+
case "visitor": {
|
|
312
|
+
const first = ctx.isFirstVisit?.();
|
|
313
|
+
if (first === void 0) return false;
|
|
314
|
+
return node.state === "first_visit" ? first : !first;
|
|
315
|
+
}
|
|
316
|
+
case "group": {
|
|
317
|
+
if (node.children.length === 0) return false;
|
|
318
|
+
return node.operator === "and" ? node.children.every((child) => evaluateCondition(child, ctx)) : node.children.some((child) => evaluateCondition(child, ctx));
|
|
319
|
+
}
|
|
320
|
+
default:
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function needsIntervalCheck(node) {
|
|
325
|
+
if (node.type === "current_time" || node.type === "user_idle") return true;
|
|
326
|
+
if (node.type === "group") return node.children.some(needsIntervalCheck);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
function needsIdleCheck(node) {
|
|
330
|
+
if (node.type === "user_idle") return true;
|
|
331
|
+
if (node.type === "group") return node.children.some(needsIdleCheck);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/flow/runtime.ts
|
|
336
|
+
var TRIGGER_THROTTLE_MS = 250;
|
|
337
|
+
var TIME_CHECK_INTERVAL_MS = 15e3;
|
|
338
|
+
var IDLE_CHECK_INTERVAL_MS = 1e3;
|
|
339
|
+
var ACTIVITY_EVENTS = [
|
|
340
|
+
"pointerdown",
|
|
341
|
+
"pointermove",
|
|
342
|
+
"keydown",
|
|
343
|
+
"wheel",
|
|
344
|
+
"scroll",
|
|
345
|
+
"touchstart"
|
|
346
|
+
];
|
|
347
|
+
var ELEMENT_WAIT_POLL_MS = 250;
|
|
348
|
+
var PAGE_READY_MAX_MS = 3e3;
|
|
349
|
+
var STEP_SETTLE_MS = 250;
|
|
350
|
+
var noopTrack = () => {
|
|
351
|
+
};
|
|
352
|
+
var defaultExecutor = () => Promise.resolve({ status: "wait" });
|
|
353
|
+
function findStepIndex(flow, cvid) {
|
|
354
|
+
return flow.steps.findIndex((step) => step.cvid === cvid);
|
|
355
|
+
}
|
|
356
|
+
function findElementTargets(node) {
|
|
357
|
+
if (!node) return [];
|
|
358
|
+
if (node.type === "element") return [node.target];
|
|
359
|
+
if (node.type === "group") return node.children.flatMap((child) => findElementTargets(child));
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
var ActiveRun = class {
|
|
363
|
+
constructor(flow, stepIndex, startedAt, flowSessionId, doc, win, runState, executor, getTrait, isFirstVisit, track, now, setTimeoutImpl, clearTimeoutImpl, setIntervalImpl, clearIntervalImpl) {
|
|
364
|
+
this.flow = flow;
|
|
365
|
+
this.stepIndex = stepIndex;
|
|
366
|
+
this.startedAt = startedAt;
|
|
367
|
+
this.flowSessionId = flowSessionId;
|
|
368
|
+
this.doc = doc;
|
|
369
|
+
this.win = win;
|
|
370
|
+
this.runState = runState;
|
|
371
|
+
this.executor = executor;
|
|
372
|
+
this.getTrait = getTrait;
|
|
373
|
+
this.isFirstVisit = isFirstVisit;
|
|
374
|
+
this.track = track;
|
|
375
|
+
this.now = now;
|
|
376
|
+
this.setTimeoutImpl = setTimeoutImpl;
|
|
377
|
+
this.clearTimeoutImpl = clearTimeoutImpl;
|
|
378
|
+
this.setIntervalImpl = setIntervalImpl;
|
|
379
|
+
this.clearIntervalImpl = clearIntervalImpl;
|
|
380
|
+
this.stopped = false;
|
|
381
|
+
this.dwellTimer = null;
|
|
382
|
+
this.observer = null;
|
|
383
|
+
this.intervalHandle = null;
|
|
384
|
+
this.throttleHandle = null;
|
|
385
|
+
this.lastCheck = 0;
|
|
386
|
+
this.trueSince = /* @__PURE__ */ new Map();
|
|
387
|
+
this.clicked = /* @__PURE__ */ new Set();
|
|
388
|
+
this.initialValues = /* @__PURE__ */ new Map();
|
|
389
|
+
this.clickListener = null;
|
|
390
|
+
/** When the end user last did anything, for `user_idle` (D1299). Reset at
|
|
391
|
+
* each step's watch start, so a step mounted on an already-quiet page
|
|
392
|
+
* starts its idle clock at zero rather than firing instantly. */
|
|
393
|
+
this.lastActivityAt = 0;
|
|
394
|
+
/** The activity listeners `startWatch` installs when a trigger tree holds
|
|
395
|
+
* a `user_idle` leaf. They stamp `lastActivityAt` and nothing else: the
|
|
396
|
+
* idle interval is what re-evaluates. */
|
|
397
|
+
this.activityListeners = [];
|
|
398
|
+
/** ADR 0166: true while a fill question is awaiting the end user's
|
|
399
|
+
* answer. Suspends trigger evaluation for the duration. */
|
|
400
|
+
this.inputPending = false;
|
|
401
|
+
/** Extra capture-phase listeners `startWatch` installs alongside the
|
|
402
|
+
* click listener (D1297): typing, focus moves and checkbox flips change
|
|
403
|
+
* nothing a MutationObserver can see, yet `text_input`, `is_focused`
|
|
404
|
+
* and `is_checked` conditions all turn on them. */
|
|
405
|
+
this.inputListeners = [];
|
|
406
|
+
/** Parsed once per step, keyed by trigger cvid, so every condition
|
|
407
|
+
* evaluation and every `is_clicked`/`user_fills_input` cache lookup
|
|
408
|
+
* within one step's lifetime shares the SAME `TargetFingerprint` object
|
|
409
|
+
* identity. `condition.ts`'s parse is otherwise pure and re-parsing on
|
|
410
|
+
* every tick would mint a fresh object each time, silently breaking the
|
|
411
|
+
* `Map`/`Set` identity these caches rely on. */
|
|
412
|
+
this.parsedTriggers = /* @__PURE__ */ new Map();
|
|
413
|
+
/** Read by anything else that wants to know why a run ended, alongside
|
|
414
|
+
* the `finish()` telemetry emission below. */
|
|
415
|
+
this.lastStopReason = null;
|
|
416
|
+
/** 26.4: the bar's status surface. Set by `beginRun` rather than passed
|
|
417
|
+
* through the constructor, which already carries fifteen positional
|
|
418
|
+
* arguments. Never throws into the run (ADR 0050). */
|
|
419
|
+
this.onStatus = null;
|
|
420
|
+
/** D1085: the shell's apology surface, fired once per run that ended on
|
|
421
|
+
* an error. Set by `beginRun`, same reasoning as `onStatus`. */
|
|
422
|
+
this.onFailure = null;
|
|
423
|
+
/** ADR 0166/D1294: withdraws a fill question still waiting in the chat
|
|
424
|
+
* surface the moment the run ends, whatever ended it. Set by `beginRun`,
|
|
425
|
+
* same reasoning as `onStatus`. Never throws into the run (ADR 0050). */
|
|
426
|
+
this.onCancelFill = null;
|
|
427
|
+
}
|
|
428
|
+
resolveElement(target, toleranceMs = 0) {
|
|
429
|
+
return matchTarget(target, this.doc);
|
|
430
|
+
}
|
|
431
|
+
/** The wait window (D1085): the flow's own `waitSeconds`, polled. Resolves
|
|
432
|
+
* early the moment the element is matched and connected, or with the last
|
|
433
|
+
* failed result once the window closes. A stopped or jumped-away run
|
|
434
|
+
* resolves immediately with whatever the DOM says right now; the caller's
|
|
435
|
+
* own staleness guard discards the outcome either way. */
|
|
436
|
+
waitForElement(target) {
|
|
437
|
+
const check = () => {
|
|
438
|
+
const result = matchTarget(target, this.doc);
|
|
439
|
+
const found = result.status === "matched" && result.element.isConnected;
|
|
440
|
+
return { result, found };
|
|
441
|
+
};
|
|
442
|
+
const first = check();
|
|
443
|
+
const windowMs = Math.max(0, this.flow.waitSeconds * 1e3);
|
|
444
|
+
if (first.found || windowMs === 0) return Promise.resolve(first.result);
|
|
445
|
+
const startIndex = this.stepIndex;
|
|
446
|
+
const deadline = this.now() + windowMs;
|
|
447
|
+
return new Promise((resolve) => {
|
|
448
|
+
const tick = () => {
|
|
449
|
+
const attempt = check();
|
|
450
|
+
if (attempt.found || this.stopped || this.stepIndex !== startIndex || this.now() >= deadline) {
|
|
451
|
+
resolve(attempt.result);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.setTimeoutImpl(tick, ELEMENT_WAIT_POLL_MS);
|
|
455
|
+
};
|
|
456
|
+
this.setTimeoutImpl(tick, ELEMENT_WAIT_POLL_MS);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
conditionContext() {
|
|
460
|
+
return {
|
|
461
|
+
doc: this.doc,
|
|
462
|
+
now: () => new Date(this.now()),
|
|
463
|
+
href: () => this.win.location?.href ?? "",
|
|
464
|
+
getTrait: this.getTrait,
|
|
465
|
+
idleSeconds: () => Math.max(0, (this.now() - this.lastActivityAt) / 1e3),
|
|
466
|
+
isFirstVisit: this.isFirstVisit,
|
|
467
|
+
wasClicked: (target) => this.clicked.has(target),
|
|
468
|
+
readInputValue: (target) => {
|
|
469
|
+
const result = matchTarget(target, this.doc);
|
|
470
|
+
if (result.status !== "matched") return void 0;
|
|
471
|
+
const el = result.element;
|
|
472
|
+
return typeof el.value === "string" ? el.value : void 0;
|
|
473
|
+
},
|
|
474
|
+
initialInputValue: (target) => {
|
|
475
|
+
if (!this.initialValues.has(target)) {
|
|
476
|
+
const result = matchTarget(target, this.doc);
|
|
477
|
+
const el = result.status === "matched" ? result.element : null;
|
|
478
|
+
this.initialValues.set(
|
|
479
|
+
target,
|
|
480
|
+
el && typeof el.value === "string" ? el.value : void 0
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return this.initialValues.get(target);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
currentStep() {
|
|
488
|
+
return this.flow.steps[this.stepIndex];
|
|
489
|
+
}
|
|
490
|
+
persist() {
|
|
491
|
+
const step = this.currentStep();
|
|
492
|
+
if (!step) return;
|
|
493
|
+
this.runState.save({
|
|
494
|
+
flowId: this.flow.cvid,
|
|
495
|
+
currentStepCvid: step.cvid,
|
|
496
|
+
flowSessionId: this.flowSessionId,
|
|
497
|
+
startedAt: this.startedAt
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
teardownWatch() {
|
|
501
|
+
this.observer?.disconnect();
|
|
502
|
+
this.observer = null;
|
|
503
|
+
if (this.intervalHandle !== null) {
|
|
504
|
+
this.clearIntervalImpl(this.intervalHandle);
|
|
505
|
+
this.intervalHandle = null;
|
|
506
|
+
}
|
|
507
|
+
if (this.throttleHandle !== null) {
|
|
508
|
+
this.clearTimeoutImpl(this.throttleHandle);
|
|
509
|
+
this.throttleHandle = null;
|
|
510
|
+
}
|
|
511
|
+
if (this.dwellTimer !== null) {
|
|
512
|
+
this.clearTimeoutImpl(this.dwellTimer);
|
|
513
|
+
this.dwellTimer = null;
|
|
514
|
+
}
|
|
515
|
+
if (this.clickListener) {
|
|
516
|
+
try {
|
|
517
|
+
this.doc.removeEventListener("click", this.clickListener, true);
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
this.clickListener = null;
|
|
521
|
+
}
|
|
522
|
+
for (const [name, listener] of this.inputListeners) {
|
|
523
|
+
try {
|
|
524
|
+
this.doc.removeEventListener(name, listener, true);
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
this.inputListeners = [];
|
|
529
|
+
for (const [name, listener] of this.activityListeners) {
|
|
530
|
+
try {
|
|
531
|
+
this.doc.removeEventListener(name, listener, true);
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
this.activityListeners = [];
|
|
536
|
+
this.inputPending = false;
|
|
537
|
+
this.trueSince.clear();
|
|
538
|
+
this.clicked.clear();
|
|
539
|
+
this.initialValues.clear();
|
|
540
|
+
}
|
|
541
|
+
jumpTo(cvid) {
|
|
542
|
+
const index = findStepIndex(this.flow, cvid);
|
|
543
|
+
if (index === -1) {
|
|
544
|
+
this.finish("error", { reason: "step_not_found" });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
this.stopStep();
|
|
548
|
+
this.stepIndex = index;
|
|
549
|
+
this.persist();
|
|
550
|
+
this.enterStep();
|
|
551
|
+
}
|
|
552
|
+
emitStatus(status) {
|
|
553
|
+
try {
|
|
554
|
+
this.onStatus?.(status);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** The end user pressed stop on the bar. The same ending a `dismiss_flow`
|
|
559
|
+
* trigger produces, so one abandoned run is one `flow_abandoned` event
|
|
560
|
+
* however it was abandoned. */
|
|
561
|
+
dismiss() {
|
|
562
|
+
if (this.stopped) return;
|
|
563
|
+
this.finish("dismissed");
|
|
564
|
+
}
|
|
565
|
+
emit(name, payload) {
|
|
566
|
+
try {
|
|
567
|
+
this.track(
|
|
568
|
+
name,
|
|
569
|
+
{
|
|
570
|
+
flowId: this.flow.cvid,
|
|
571
|
+
flowSessionId: this.flowSessionId,
|
|
572
|
+
stepCvid: this.currentStep()?.cvid
|
|
573
|
+
},
|
|
574
|
+
payload
|
|
575
|
+
);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
finish(reason, payload) {
|
|
580
|
+
this.lastStopReason = reason;
|
|
581
|
+
const eventName = reason === "completed" ? "flow_completed" : reason === "dismissed" ? "flow_abandoned" : "flow_error";
|
|
582
|
+
this.emit(eventName, payload);
|
|
583
|
+
try {
|
|
584
|
+
this.onCancelFill?.();
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
this.stopStep();
|
|
588
|
+
this.stopped = true;
|
|
589
|
+
this.runState.clear();
|
|
590
|
+
if (reason === "completed") {
|
|
591
|
+
this.emitStatus({
|
|
592
|
+
name: this.flow.publicName ?? this.flow.name,
|
|
593
|
+
step: this.flow.steps.length,
|
|
594
|
+
total: this.flow.steps.length,
|
|
595
|
+
phase: "done"
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
this.emitStatus(null);
|
|
599
|
+
if (reason === "error") {
|
|
600
|
+
try {
|
|
601
|
+
this.onFailure?.();
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
stopStep() {
|
|
607
|
+
this.teardownWatch();
|
|
608
|
+
}
|
|
609
|
+
fireTrigger(trigger) {
|
|
610
|
+
if (trigger.actionType === "dismiss_flow") {
|
|
611
|
+
this.finish("dismissed");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (trigger.actionType === "go_to_step" && trigger.targetStepCvid) {
|
|
615
|
+
this.jumpTo(trigger.targetStepCvid);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
parsedTrigger(trigger) {
|
|
619
|
+
if (!this.parsedTriggers.has(trigger.cvid)) {
|
|
620
|
+
this.parsedTriggers.set(trigger.cvid, parseTriggerCondition(trigger.condition));
|
|
621
|
+
}
|
|
622
|
+
return this.parsedTriggers.get(trigger.cvid) ?? null;
|
|
623
|
+
}
|
|
624
|
+
checkTriggers(step, ctx) {
|
|
625
|
+
if (this.stopped) return;
|
|
626
|
+
if (this.inputPending) return;
|
|
627
|
+
const now = this.now();
|
|
628
|
+
for (const trigger of [...step.triggers].sort((a, b) => a.position - b.position)) {
|
|
629
|
+
const parsed = this.parsedTrigger(trigger);
|
|
630
|
+
if (!parsed) continue;
|
|
631
|
+
const isTrue = evaluateCondition(parsed.node, ctx);
|
|
632
|
+
const key = trigger.cvid;
|
|
633
|
+
if (!isTrue) {
|
|
634
|
+
this.trueSince.delete(key);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const since = this.trueSince.get(key) ?? now;
|
|
638
|
+
if (!this.trueSince.has(key)) this.trueSince.set(key, since);
|
|
639
|
+
const waitMs = (trigger.waitSeconds ?? 0) * 1e3;
|
|
640
|
+
if (now - since >= waitMs) {
|
|
641
|
+
this.fireTrigger(trigger);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
scheduleCheck(step) {
|
|
647
|
+
if (this.stopped || this.throttleHandle !== null) return;
|
|
648
|
+
const wait = Math.max(0, TRIGGER_THROTTLE_MS - (this.now() - this.lastCheck));
|
|
649
|
+
this.throttleHandle = this.setTimeoutImpl(() => {
|
|
650
|
+
this.throttleHandle = null;
|
|
651
|
+
this.lastCheck = this.now();
|
|
652
|
+
this.checkTriggers(step, this.conditionContext());
|
|
653
|
+
}, wait);
|
|
654
|
+
}
|
|
655
|
+
startWatch(step) {
|
|
656
|
+
const anyNeedsInterval = step.triggers.some((trigger) => {
|
|
657
|
+
const parsed = this.parsedTrigger(trigger);
|
|
658
|
+
return parsed ? needsIntervalCheck(parsed.node) : false;
|
|
659
|
+
});
|
|
660
|
+
const anyNeedsIdle = step.triggers.some((trigger) => {
|
|
661
|
+
const parsed = this.parsedTrigger(trigger);
|
|
662
|
+
return parsed ? needsIdleCheck(parsed.node) : false;
|
|
663
|
+
});
|
|
664
|
+
this.lastActivityAt = this.now();
|
|
665
|
+
if (typeof MutationObserver === "function") {
|
|
666
|
+
try {
|
|
667
|
+
this.observer = new MutationObserver(() => this.scheduleCheck(step));
|
|
668
|
+
this.observer.observe(this.doc.documentElement, {
|
|
669
|
+
childList: true,
|
|
670
|
+
subtree: true,
|
|
671
|
+
attributes: true,
|
|
672
|
+
characterData: true
|
|
673
|
+
});
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (anyNeedsInterval) {
|
|
678
|
+
try {
|
|
679
|
+
this.intervalHandle = this.setIntervalImpl(
|
|
680
|
+
() => this.scheduleCheck(step),
|
|
681
|
+
anyNeedsIdle ? IDLE_CHECK_INTERVAL_MS : TIME_CHECK_INTERVAL_MS
|
|
682
|
+
);
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (anyNeedsIdle) {
|
|
687
|
+
for (const name of ACTIVITY_EVENTS) {
|
|
688
|
+
const listener = () => {
|
|
689
|
+
this.lastActivityAt = this.now();
|
|
690
|
+
};
|
|
691
|
+
try {
|
|
692
|
+
this.doc.addEventListener(name, listener, { capture: true, passive: true });
|
|
693
|
+
this.activityListeners.push([name, listener]);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
this.clickListener = (event) => {
|
|
699
|
+
for (const trigger of step.triggers) {
|
|
700
|
+
const parsed = this.parsedTrigger(trigger);
|
|
701
|
+
const target = findElementTargets(parsed?.node);
|
|
702
|
+
for (const t of target) {
|
|
703
|
+
const result = matchTarget(t, this.doc);
|
|
704
|
+
if (result.status === "matched" && event.target instanceof Node && (result.element === event.target || result.element.contains(event.target))) {
|
|
705
|
+
this.clicked.add(t);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
this.scheduleCheck(step);
|
|
710
|
+
};
|
|
711
|
+
try {
|
|
712
|
+
this.doc.addEventListener("click", this.clickListener, true);
|
|
713
|
+
} catch {
|
|
714
|
+
this.clickListener = null;
|
|
715
|
+
}
|
|
716
|
+
for (const name of ["input", "change", "focusin", "focusout"]) {
|
|
717
|
+
const listener = () => this.scheduleCheck(step);
|
|
718
|
+
try {
|
|
719
|
+
this.doc.addEventListener(name, listener, true);
|
|
720
|
+
this.inputListeners.push([name, listener]);
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
this.checkTriggers(step, this.conditionContext());
|
|
725
|
+
}
|
|
726
|
+
async runExecutor(step) {
|
|
727
|
+
const startIndex = this.stepIndex;
|
|
728
|
+
try {
|
|
729
|
+
const outcome = await this.executor(step, {
|
|
730
|
+
doc: this.doc,
|
|
731
|
+
win: this.win,
|
|
732
|
+
resolveElement: (target, toleranceMs) => this.resolveElement(target, toleranceMs),
|
|
733
|
+
waitForElement: (target) => this.waitForElement(target),
|
|
734
|
+
setInputPending: (pending) => {
|
|
735
|
+
this.inputPending = pending;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
if (this.stopped || this.stepIndex !== startIndex) return;
|
|
739
|
+
if (outcome.status === "advance") {
|
|
740
|
+
this.emit("flow_step_completed");
|
|
741
|
+
if (step.isCompletion) {
|
|
742
|
+
this.finish("completed");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const nextIndex = this.stepIndex + 1;
|
|
746
|
+
if (nextIndex >= this.flow.steps.length) {
|
|
747
|
+
this.finish("completed");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
this.stopStep();
|
|
751
|
+
this.stepIndex = nextIndex;
|
|
752
|
+
this.persist();
|
|
753
|
+
this.enterStep();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (outcome.status === "navigated") {
|
|
757
|
+
this.emit("flow_step_completed");
|
|
758
|
+
if (outcome.navigateTarget === "new_tab") {
|
|
759
|
+
this.finish("completed");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
this.stopStep();
|
|
763
|
+
if (step.isCompletion) {
|
|
764
|
+
this.finish("completed");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const nextIndex = this.stepIndex + 1;
|
|
768
|
+
if (nextIndex >= this.flow.steps.length) {
|
|
769
|
+
this.finish("completed");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.stepIndex = nextIndex;
|
|
773
|
+
this.persist();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (outcome.status === "error") {
|
|
777
|
+
this.finish(
|
|
778
|
+
"error",
|
|
779
|
+
outcome.reason === "element_not_found" ? { reason: outcome.reason, liveManifest: captureLiveManifest(this.doc) } : { reason: outcome.reason }
|
|
780
|
+
);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
enterStep() {
|
|
787
|
+
const step = this.currentStep();
|
|
788
|
+
if (!step || this.stopped) {
|
|
789
|
+
this.finish(step ? "completed" : "error", step ? void 0 : { reason: "missing_step" });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.persist();
|
|
793
|
+
this.emit("flow_step_entered");
|
|
794
|
+
this.emitStatus({
|
|
795
|
+
name: this.flow.publicName ?? this.flow.name,
|
|
796
|
+
step: this.stepIndex + 1,
|
|
797
|
+
total: this.flow.steps.length,
|
|
798
|
+
phase: "running"
|
|
799
|
+
});
|
|
800
|
+
const begin = () => {
|
|
801
|
+
if (this.stopped) return;
|
|
802
|
+
this.startWatch(step);
|
|
803
|
+
void this.runExecutor(step);
|
|
804
|
+
};
|
|
805
|
+
this.waitForPageReady(begin);
|
|
806
|
+
}
|
|
807
|
+
/** Holds until the document is past parsing, then one `STEP_SETTLE_MS`
|
|
808
|
+
* beat, then runs `begin`. Gives up waiting on `readyState` after
|
|
809
|
+
* `PAGE_READY_MAX_MS` and starts the step regardless: a page that slow is
|
|
810
|
+
* `waitForElement`'s problem, and it has the flow's full wait window for
|
|
811
|
+
* it. Uses the same `dwellTimer` slot as the old dwell, so `stopStep`
|
|
812
|
+
* cancels a pending entry exactly as before. */
|
|
813
|
+
waitForPageReady(begin) {
|
|
814
|
+
const deadline = this.now() + PAGE_READY_MAX_MS;
|
|
815
|
+
const settleThenBegin = () => {
|
|
816
|
+
this.dwellTimer = this.setTimeoutImpl(() => {
|
|
817
|
+
this.dwellTimer = null;
|
|
818
|
+
begin();
|
|
819
|
+
}, STEP_SETTLE_MS);
|
|
820
|
+
};
|
|
821
|
+
const check = () => {
|
|
822
|
+
if (this.stopped) return;
|
|
823
|
+
if (this.doc.readyState !== "loading" || this.now() >= deadline) {
|
|
824
|
+
settleThenBegin();
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
this.dwellTimer = this.setTimeoutImpl(() => {
|
|
828
|
+
this.dwellTimer = null;
|
|
829
|
+
check();
|
|
830
|
+
}, ELEMENT_WAIT_POLL_MS);
|
|
831
|
+
};
|
|
832
|
+
check();
|
|
833
|
+
}
|
|
834
|
+
begin() {
|
|
835
|
+
this.enterStep();
|
|
836
|
+
}
|
|
837
|
+
stop() {
|
|
838
|
+
if (this.stopped) return;
|
|
839
|
+
try {
|
|
840
|
+
this.onCancelFill?.();
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
this.stopStep();
|
|
844
|
+
this.stopped = true;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
function createFlowRuntime(options) {
|
|
848
|
+
const {
|
|
849
|
+
context,
|
|
850
|
+
executor = defaultExecutor,
|
|
851
|
+
track = context.track ?? noopTrack,
|
|
852
|
+
now = () => Date.now(),
|
|
853
|
+
setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
|
|
854
|
+
clearTimeoutImpl = (handle) => clearTimeout(handle),
|
|
855
|
+
setIntervalImpl = (fn, ms) => setInterval(fn, ms),
|
|
856
|
+
clearIntervalImpl = (handle) => clearInterval(handle)
|
|
857
|
+
} = options;
|
|
858
|
+
const doc = context.doc ?? document;
|
|
859
|
+
const win = context.win ?? window;
|
|
860
|
+
const runState = createFlowRunState(context.token);
|
|
861
|
+
const apiOptions = {
|
|
862
|
+
apiBase: context.apiBase,
|
|
863
|
+
sessionToken: context.sessionToken,
|
|
864
|
+
sessionId: context.sessionId
|
|
865
|
+
};
|
|
866
|
+
let active = null;
|
|
867
|
+
async function loadFlow(flowCvid) {
|
|
868
|
+
const result = await fetchFlowRun(flowCvid, apiOptions);
|
|
869
|
+
return result.ok ? result.flow : null;
|
|
870
|
+
}
|
|
871
|
+
function beginRun(flow, stepIndex, startedAt, flowSessionId, overrides = {}) {
|
|
872
|
+
active?.stop();
|
|
873
|
+
active = new ActiveRun(
|
|
874
|
+
flow,
|
|
875
|
+
stepIndex,
|
|
876
|
+
startedAt,
|
|
877
|
+
flowSessionId,
|
|
878
|
+
doc,
|
|
879
|
+
win,
|
|
880
|
+
overrides.runState ?? runState,
|
|
881
|
+
executor,
|
|
882
|
+
context.getTrait,
|
|
883
|
+
context.isFirstVisit,
|
|
884
|
+
overrides.track ?? track,
|
|
885
|
+
now,
|
|
886
|
+
setTimeoutImpl,
|
|
887
|
+
clearTimeoutImpl,
|
|
888
|
+
setIntervalImpl,
|
|
889
|
+
clearIntervalImpl
|
|
890
|
+
);
|
|
891
|
+
active.onStatus = (status) => context.onStatus?.(status);
|
|
892
|
+
active.onFailure = () => context.onFailure?.();
|
|
893
|
+
active.onCancelFill = () => context.cancelFill?.();
|
|
894
|
+
active.begin();
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
async start(flowCvid) {
|
|
898
|
+
const flow = await loadFlow(flowCvid);
|
|
899
|
+
if (!flow || flow.steps.length === 0) return;
|
|
900
|
+
const flowSessionId = uuidV4();
|
|
901
|
+
try {
|
|
902
|
+
track("flow_started", { flowId: flow.cvid, flowSessionId });
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
beginRun(flow, 0, now(), flowSessionId);
|
|
906
|
+
},
|
|
907
|
+
async resume() {
|
|
908
|
+
const pending = runState.read();
|
|
909
|
+
if (!pending) return false;
|
|
910
|
+
const flow = await loadFlow(pending.flowId);
|
|
911
|
+
if (!flow || flow.steps.length === 0) {
|
|
912
|
+
runState.clear();
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
const stepIndex = findStepIndex(flow, pending.currentStepCvid);
|
|
916
|
+
if (stepIndex === -1) {
|
|
917
|
+
runState.clear();
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
beginRun(flow, stepIndex, pending.startedAt, pending.flowSessionId);
|
|
921
|
+
return true;
|
|
922
|
+
},
|
|
923
|
+
stop() {
|
|
924
|
+
active?.dismiss();
|
|
925
|
+
active = null;
|
|
926
|
+
},
|
|
927
|
+
// ── Preview (ADR 0137, D1081) ────────────────────────────────────────
|
|
928
|
+
// A real run through the exact same ActiveRun, differing in only two
|
|
929
|
+
// injected seams: run state persists to `arcy.preview.<token>`
|
|
930
|
+
// (sessionStorage, carrying the draft itself, since the spent nonce
|
|
931
|
+
// cannot serve it twice), and telemetry is `noopTrack`, so a preview
|
|
932
|
+
// never touches funnel numbers even if the page-level telemetry stop
|
|
933
|
+
// were somehow missed. Everything else - executor, promptFill, status
|
|
934
|
+
// surface, failure stop - is the production path, which is the whole
|
|
935
|
+
// point of previewing.
|
|
936
|
+
async startPreview(raw) {
|
|
937
|
+
const flow = readFlowDetail(raw);
|
|
938
|
+
if (!flow || flow.steps.length === 0) return;
|
|
939
|
+
const previewState = createPreviewRunState(context.token);
|
|
940
|
+
beginRun(flow, 0, now(), uuidV4(), {
|
|
941
|
+
runState: previewStateAdapter(previewState, raw),
|
|
942
|
+
track: noopTrack
|
|
943
|
+
});
|
|
944
|
+
},
|
|
945
|
+
async resumePreview() {
|
|
946
|
+
const previewState = createPreviewRunState(context.token);
|
|
947
|
+
const pending = previewState.read();
|
|
948
|
+
if (!pending) return false;
|
|
949
|
+
const flow = readFlowDetail(pending.flow);
|
|
950
|
+
if (!flow || flow.steps.length === 0) {
|
|
951
|
+
previewState.clear();
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
const stepIndex = findStepIndex(flow, pending.currentStepCvid);
|
|
955
|
+
if (stepIndex === -1) {
|
|
956
|
+
previewState.clear();
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
beginRun(flow, stepIndex, pending.startedAt, pending.flowSessionId, {
|
|
960
|
+
runState: previewStateAdapter(previewState, pending.flow),
|
|
961
|
+
track: noopTrack
|
|
962
|
+
});
|
|
963
|
+
return true;
|
|
964
|
+
},
|
|
965
|
+
destroy() {
|
|
966
|
+
active?.stop();
|
|
967
|
+
active = null;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function previewStateAdapter(previewState, rawFlow) {
|
|
972
|
+
return {
|
|
973
|
+
// ActiveRun only ever writes; reads happen in resumePreview() above.
|
|
974
|
+
read: () => null,
|
|
975
|
+
save: (run) => previewState.save({ ...run, flow: rawFlow }),
|
|
976
|
+
clear: () => previewState.clear()
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/flow/executor.ts
|
|
981
|
+
var SCROLL_MARGIN = 24;
|
|
982
|
+
function scrollOffsetFor(rect, viewportHeight, occlusion) {
|
|
983
|
+
const floor = viewportHeight - Math.max(0, occlusion);
|
|
984
|
+
if (!(viewportHeight > 0) || floor <= 0) return 0;
|
|
985
|
+
if (rect.bottom > floor) {
|
|
986
|
+
const needed = rect.bottom - floor + SCROLL_MARGIN;
|
|
987
|
+
const room = Math.max(0, rect.top - SCROLL_MARGIN);
|
|
988
|
+
return Math.min(needed, room);
|
|
989
|
+
}
|
|
990
|
+
if (rect.top < 0) return rect.top - SCROLL_MARGIN;
|
|
991
|
+
return 0;
|
|
992
|
+
}
|
|
993
|
+
function scrollIntoViewAboveBar(el, ctx, options) {
|
|
994
|
+
try {
|
|
995
|
+
const occlusion = options.getBottomOcclusion?.() ?? 0;
|
|
996
|
+
const rect = el.getBoundingClientRect();
|
|
997
|
+
const height = ctx.doc.documentElement?.clientHeight || ctx.win.innerHeight || 0;
|
|
998
|
+
const delta = scrollOffsetFor(rect, height, occlusion);
|
|
999
|
+
if (delta === 0) return;
|
|
1000
|
+
ctx.win.scrollBy({ top: delta, behavior: "smooth" });
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function dispatchClickSequence(el, doc) {
|
|
1005
|
+
const win = doc.defaultView;
|
|
1006
|
+
const PointerCtor = win?.PointerEvent;
|
|
1007
|
+
const rect = (() => {
|
|
1008
|
+
try {
|
|
1009
|
+
return el.getBoundingClientRect();
|
|
1010
|
+
} catch {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
})();
|
|
1014
|
+
const point = {
|
|
1015
|
+
bubbles: true,
|
|
1016
|
+
cancelable: true,
|
|
1017
|
+
clientX: rect ? rect.left + rect.width / 2 : 0,
|
|
1018
|
+
clientY: rect ? rect.top + rect.height / 2 : 0
|
|
1019
|
+
};
|
|
1020
|
+
const firePointer = (type) => {
|
|
1021
|
+
try {
|
|
1022
|
+
if (typeof PointerCtor === "function") {
|
|
1023
|
+
el.dispatchEvent(new PointerCtor(type, { ...point, pointerType: "mouse" }));
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
const fireMouse = (type) => {
|
|
1029
|
+
try {
|
|
1030
|
+
el.dispatchEvent(new MouseEvent(type, point));
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
firePointer("pointerover");
|
|
1035
|
+
firePointer("pointerenter");
|
|
1036
|
+
firePointer("pointerdown");
|
|
1037
|
+
fireMouse("mousedown");
|
|
1038
|
+
try {
|
|
1039
|
+
if (typeof el.focus === "function") el.focus();
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
firePointer("pointerup");
|
|
1043
|
+
fireMouse("mouseup");
|
|
1044
|
+
fireMouse("click");
|
|
1045
|
+
}
|
|
1046
|
+
function setNativeValue(el, value) {
|
|
1047
|
+
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLInputElement ? HTMLInputElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : null;
|
|
1048
|
+
if (!proto) return false;
|
|
1049
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
|
|
1050
|
+
const setter = descriptor?.set;
|
|
1051
|
+
if (!setter) return false;
|
|
1052
|
+
try {
|
|
1053
|
+
setter.call(el, value);
|
|
1054
|
+
return true;
|
|
1055
|
+
} catch {
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function dispatchInputChange(el, doc) {
|
|
1060
|
+
const win = doc.defaultView;
|
|
1061
|
+
try {
|
|
1062
|
+
el.dispatchEvent(new (win?.Event ?? Event)("input", { bubbles: true }));
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
el.dispatchEvent(new (win?.Event ?? Event)("change", { bubbles: true }));
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
async function dispatchClickStep(step, ctx, options) {
|
|
1071
|
+
if (!isTargetFingerprint(step.target)) {
|
|
1072
|
+
return { status: "error", reason: "invalid_target" };
|
|
1073
|
+
}
|
|
1074
|
+
const result = await ctx.waitForElement(step.target);
|
|
1075
|
+
if (result.status !== "matched" || !result.element.isConnected) {
|
|
1076
|
+
return { status: "error", reason: "element_not_found" };
|
|
1077
|
+
}
|
|
1078
|
+
scrollIntoViewAboveBar(result.element, ctx, options);
|
|
1079
|
+
try {
|
|
1080
|
+
dispatchClickSequence(result.element, ctx.doc);
|
|
1081
|
+
} catch {
|
|
1082
|
+
return { status: "error", reason: "dispatch_failed" };
|
|
1083
|
+
}
|
|
1084
|
+
return { status: "advance" };
|
|
1085
|
+
}
|
|
1086
|
+
var AUTHORED_INPUT_TYPES = [
|
|
1087
|
+
"text",
|
|
1088
|
+
"number",
|
|
1089
|
+
"email",
|
|
1090
|
+
"date",
|
|
1091
|
+
"url",
|
|
1092
|
+
"tel"
|
|
1093
|
+
];
|
|
1094
|
+
function readAuthoredInputType(value) {
|
|
1095
|
+
return AUTHORED_INPUT_TYPES.includes(value) ? value : null;
|
|
1096
|
+
}
|
|
1097
|
+
var INPUT_TYPE_TO_KIND = {
|
|
1098
|
+
text: "text",
|
|
1099
|
+
search: "text",
|
|
1100
|
+
number: "number",
|
|
1101
|
+
email: "email",
|
|
1102
|
+
url: "url",
|
|
1103
|
+
tel: "tel",
|
|
1104
|
+
password: "password",
|
|
1105
|
+
date: "date",
|
|
1106
|
+
time: "time",
|
|
1107
|
+
"datetime-local": "datetime",
|
|
1108
|
+
month: "month",
|
|
1109
|
+
week: "week",
|
|
1110
|
+
checkbox: "checkbox",
|
|
1111
|
+
radio: "radio",
|
|
1112
|
+
range: "range"
|
|
1113
|
+
};
|
|
1114
|
+
function readAttr(el, name) {
|
|
1115
|
+
try {
|
|
1116
|
+
const value = el.getAttribute(name);
|
|
1117
|
+
return value === null || value === "" ? null : value;
|
|
1118
|
+
} catch {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function selectOptions(el) {
|
|
1123
|
+
const options = [];
|
|
1124
|
+
for (let i = 0; i < el.options.length; i += 1) {
|
|
1125
|
+
const option = el.options[i];
|
|
1126
|
+
if (!option || option.disabled) continue;
|
|
1127
|
+
const label = (option.label || option.textContent || "").trim();
|
|
1128
|
+
if (option.value === "") continue;
|
|
1129
|
+
options.push({ value: option.value, label: label || option.value });
|
|
1130
|
+
}
|
|
1131
|
+
return options;
|
|
1132
|
+
}
|
|
1133
|
+
function radioOptions(el, doc) {
|
|
1134
|
+
const name = el.getAttribute("name");
|
|
1135
|
+
const scope = el.form ?? doc;
|
|
1136
|
+
const group = [];
|
|
1137
|
+
if (name) {
|
|
1138
|
+
try {
|
|
1139
|
+
const selector = `input[type="radio"][name="${CSS.escape(name)}"]`;
|
|
1140
|
+
scope.querySelectorAll(selector).forEach((node) => {
|
|
1141
|
+
if (node instanceof HTMLInputElement) group.push(node);
|
|
1142
|
+
});
|
|
1143
|
+
} catch {
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (group.length === 0) group.push(el);
|
|
1147
|
+
return group.map((radio) => ({
|
|
1148
|
+
value: radio.value,
|
|
1149
|
+
label: radioLabel(radio) || radio.value
|
|
1150
|
+
}));
|
|
1151
|
+
}
|
|
1152
|
+
function controlLabelText(el) {
|
|
1153
|
+
try {
|
|
1154
|
+
const labels = el.labels;
|
|
1155
|
+
if (labels && labels.length > 0) {
|
|
1156
|
+
return (labels[0]?.textContent ?? "").trim();
|
|
1157
|
+
}
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
try {
|
|
1161
|
+
const closest = el.closest("label");
|
|
1162
|
+
if (closest) return (closest.textContent ?? "").trim();
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
return "";
|
|
1166
|
+
}
|
|
1167
|
+
function radioLabel(el) {
|
|
1168
|
+
return controlLabelText(el);
|
|
1169
|
+
}
|
|
1170
|
+
function readFillSpec(el, authored, doc) {
|
|
1171
|
+
let kind = "custom";
|
|
1172
|
+
let options = null;
|
|
1173
|
+
let required = false;
|
|
1174
|
+
let min = null;
|
|
1175
|
+
let max = null;
|
|
1176
|
+
let step = null;
|
|
1177
|
+
let maxLength = null;
|
|
1178
|
+
let pattern = null;
|
|
1179
|
+
if (el instanceof HTMLSelectElement) {
|
|
1180
|
+
kind = "select";
|
|
1181
|
+
options = selectOptions(el);
|
|
1182
|
+
required = el.required || !el.querySelector('option[value=""]');
|
|
1183
|
+
} else if (el instanceof HTMLTextAreaElement) {
|
|
1184
|
+
kind = "textarea";
|
|
1185
|
+
required = el.required;
|
|
1186
|
+
if (el.maxLength > 0) maxLength = el.maxLength;
|
|
1187
|
+
} else if (el instanceof HTMLInputElement) {
|
|
1188
|
+
const type = (readAttr(el, "type") ?? "text").toLowerCase();
|
|
1189
|
+
kind = INPUT_TYPE_TO_KIND[type] ?? "text";
|
|
1190
|
+
required = el.required;
|
|
1191
|
+
min = readAttr(el, "min");
|
|
1192
|
+
max = readAttr(el, "max");
|
|
1193
|
+
step = readAttr(el, "step");
|
|
1194
|
+
pattern = readAttr(el, "pattern");
|
|
1195
|
+
if (el.maxLength > 0) maxLength = el.maxLength;
|
|
1196
|
+
if (kind === "radio") options = radioOptions(el, doc);
|
|
1197
|
+
if (kind === "checkbox") {
|
|
1198
|
+
options = [{ value: "true", label: controlLabelText(el) }];
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (authored && authored !== "text" && (kind === "text" || kind === "custom" || kind === "textarea")) {
|
|
1202
|
+
kind = authored;
|
|
1203
|
+
}
|
|
1204
|
+
return { kind, required, options, min, max, step, maxLength, pattern };
|
|
1205
|
+
}
|
|
1206
|
+
function findEditable(el) {
|
|
1207
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
1208
|
+
return el;
|
|
1209
|
+
}
|
|
1210
|
+
if (el instanceof HTMLElement && el.isContentEditable) return el;
|
|
1211
|
+
try {
|
|
1212
|
+
const inner = el.querySelector(
|
|
1213
|
+
'input:not([type="hidden"]), textarea, select, [contenteditable="true"], [contenteditable=""]'
|
|
1214
|
+
);
|
|
1215
|
+
return inner instanceof HTMLElement ? inner : null;
|
|
1216
|
+
} catch {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function dispatchKeyboardShadow(el, doc) {
|
|
1221
|
+
const win = doc.defaultView;
|
|
1222
|
+
const Ctor = win?.KeyboardEvent;
|
|
1223
|
+
if (typeof Ctor !== "function") return;
|
|
1224
|
+
for (const type of ["keydown", "keyup"]) {
|
|
1225
|
+
try {
|
|
1226
|
+
el.dispatchEvent(new Ctor(type, { bubbles: true, cancelable: true }));
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function applyFillReply(el, reply, kind, doc) {
|
|
1232
|
+
if (kind === "checkbox" && el instanceof HTMLInputElement) {
|
|
1233
|
+
const desired = reply === "true";
|
|
1234
|
+
if (el.checked !== desired) dispatchClickSequence(el, doc);
|
|
1235
|
+
if (el.checked !== desired) {
|
|
1236
|
+
el.checked = desired;
|
|
1237
|
+
dispatchInputChange(el, doc);
|
|
1238
|
+
}
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
if (kind === "radio" && el instanceof HTMLInputElement) {
|
|
1242
|
+
const scope = el.form ?? doc;
|
|
1243
|
+
const name = el.getAttribute("name");
|
|
1244
|
+
let chosen = null;
|
|
1245
|
+
if (name) {
|
|
1246
|
+
try {
|
|
1247
|
+
const selector = `input[type="radio"][name="${CSS.escape(name)}"]`;
|
|
1248
|
+
scope.querySelectorAll(selector).forEach((node) => {
|
|
1249
|
+
if (node instanceof HTMLInputElement && node.value === reply) {
|
|
1250
|
+
chosen = node;
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
} catch {
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const target = chosen ?? (el.value === reply ? el : null);
|
|
1257
|
+
if (!target) return false;
|
|
1258
|
+
dispatchClickSequence(target, doc);
|
|
1259
|
+
const settled = target;
|
|
1260
|
+
if (!settled.checked) {
|
|
1261
|
+
settled.checked = true;
|
|
1262
|
+
dispatchInputChange(settled, doc);
|
|
1263
|
+
}
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
const editable = findEditable(el);
|
|
1267
|
+
if (!editable) return false;
|
|
1268
|
+
if (editable instanceof HTMLElement && editable.isContentEditable) {
|
|
1269
|
+
try {
|
|
1270
|
+
editable.focus();
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
editable.textContent = reply;
|
|
1275
|
+
} catch {
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
dispatchInputChange(editable, doc);
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
editable.focus();
|
|
1283
|
+
} catch {
|
|
1284
|
+
}
|
|
1285
|
+
if (!setNativeValue(editable, reply)) return false;
|
|
1286
|
+
dispatchKeyboardShadow(editable, doc);
|
|
1287
|
+
dispatchInputChange(editable, doc);
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
async function dispatchFillStep(step, ctx, options) {
|
|
1291
|
+
if (!isTargetFingerprint(step.target)) {
|
|
1292
|
+
return { status: "error", reason: "invalid_target" };
|
|
1293
|
+
}
|
|
1294
|
+
if (!options.promptFill) {
|
|
1295
|
+
return { status: "wait" };
|
|
1296
|
+
}
|
|
1297
|
+
const result = await ctx.waitForElement(step.target);
|
|
1298
|
+
if (result.status !== "matched" || !result.element.isConnected) {
|
|
1299
|
+
return { status: "error", reason: "element_not_found" };
|
|
1300
|
+
}
|
|
1301
|
+
const spec = readFillSpec(
|
|
1302
|
+
result.element,
|
|
1303
|
+
readAuthoredInputType(step.fillInputType),
|
|
1304
|
+
ctx.doc
|
|
1305
|
+
);
|
|
1306
|
+
const question = (step.fillDescription ?? "").trim() || step.label || "";
|
|
1307
|
+
let errorCode = null;
|
|
1308
|
+
let lastInvalid = null;
|
|
1309
|
+
let reply;
|
|
1310
|
+
for (; ; ) {
|
|
1311
|
+
let answer;
|
|
1312
|
+
ctx.setInputPending(true);
|
|
1313
|
+
try {
|
|
1314
|
+
answer = await options.promptFill({ ...spec, question, errorCode });
|
|
1315
|
+
} catch {
|
|
1316
|
+
return { status: "error", reason: "prompt_failed" };
|
|
1317
|
+
} finally {
|
|
1318
|
+
ctx.setInputPending(false);
|
|
1319
|
+
}
|
|
1320
|
+
if (answer === null) {
|
|
1321
|
+
return { status: "error", reason: "fill_cancelled" };
|
|
1322
|
+
}
|
|
1323
|
+
const normalized = spec.kind === "select" || spec.kind === "radio" ? normalizeOptionReply(answer, spec.options) : spec.kind === "password" ? (
|
|
1324
|
+
// Never trimmed: a password's whitespace is the user's own.
|
|
1325
|
+
answer
|
|
1326
|
+
) : answer.trim();
|
|
1327
|
+
errorCode = validateFillReply(normalized, { ...spec, question, errorCode: null });
|
|
1328
|
+
if (errorCode === null) {
|
|
1329
|
+
reply = normalized;
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
if (lastInvalid !== null && lastInvalid === normalized) {
|
|
1333
|
+
return { status: "error", reason: "fill_invalid" };
|
|
1334
|
+
}
|
|
1335
|
+
lastInvalid = normalized;
|
|
1336
|
+
}
|
|
1337
|
+
const fresh = await ctx.waitForElement(step.target);
|
|
1338
|
+
if (fresh.status !== "matched" || !fresh.element.isConnected) {
|
|
1339
|
+
return { status: "error", reason: "element_not_found" };
|
|
1340
|
+
}
|
|
1341
|
+
const freshSpec = readFillSpec(
|
|
1342
|
+
fresh.element,
|
|
1343
|
+
readAuthoredInputType(step.fillInputType),
|
|
1344
|
+
ctx.doc
|
|
1345
|
+
);
|
|
1346
|
+
if (freshSpec.kind !== spec.kind) {
|
|
1347
|
+
return { status: "error", reason: "unfillable_target" };
|
|
1348
|
+
}
|
|
1349
|
+
scrollIntoViewAboveBar(fresh.element, ctx, options);
|
|
1350
|
+
if (!applyFillReply(fresh.element, reply, freshSpec.kind, ctx.doc)) {
|
|
1351
|
+
return { status: "error", reason: "unfillable_target" };
|
|
1352
|
+
}
|
|
1353
|
+
return { status: "advance" };
|
|
1354
|
+
}
|
|
1355
|
+
function isSafeNavigateUrl(url) {
|
|
1356
|
+
if (url.startsWith("/") && !url.startsWith("//")) return true;
|
|
1357
|
+
try {
|
|
1358
|
+
const parsed = new URL(url);
|
|
1359
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
1360
|
+
} catch {
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async function dispatchNavigateStep(step, ctx) {
|
|
1365
|
+
if (!step.navigateUrl || !step.navigateTarget) {
|
|
1366
|
+
return { status: "error", reason: "invalid_navigate_step" };
|
|
1367
|
+
}
|
|
1368
|
+
if (!isSafeNavigateUrl(step.navigateUrl)) {
|
|
1369
|
+
return { status: "error", reason: "invalid_navigate_step" };
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
if (step.navigateTarget === "new_tab") {
|
|
1373
|
+
ctx.win.open(step.navigateUrl, "_blank", "noopener,noreferrer");
|
|
1374
|
+
} else {
|
|
1375
|
+
ctx.win.location.assign(step.navigateUrl);
|
|
1376
|
+
}
|
|
1377
|
+
} catch {
|
|
1378
|
+
return { status: "error", reason: "navigate_failed" };
|
|
1379
|
+
}
|
|
1380
|
+
return { status: "navigated", navigateTarget: step.navigateTarget };
|
|
1381
|
+
}
|
|
1382
|
+
function createDomExecutor(options = {}) {
|
|
1383
|
+
return async (step, ctx) => {
|
|
1384
|
+
try {
|
|
1385
|
+
switch (step.type) {
|
|
1386
|
+
case "click":
|
|
1387
|
+
return await dispatchClickStep(step, ctx, options);
|
|
1388
|
+
case "fill":
|
|
1389
|
+
return await dispatchFillStep(step, ctx, options);
|
|
1390
|
+
case "navigate":
|
|
1391
|
+
return await dispatchNavigateStep(step, ctx);
|
|
1392
|
+
default:
|
|
1393
|
+
return { status: "error", reason: "unknown_step_type" };
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1396
|
+
return { status: "error", reason: "executor_threw" };
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/flow/index.ts
|
|
1402
|
+
function mount(context) {
|
|
1403
|
+
try {
|
|
1404
|
+
const executor = createDomExecutor({
|
|
1405
|
+
promptFill: context.promptFill,
|
|
1406
|
+
// 26.4: the bar covers the bottom of the screen, so a target under it
|
|
1407
|
+
// is scrolled into view before it is clicked or filled.
|
|
1408
|
+
getBottomOcclusion: context.getBottomOcclusion
|
|
1409
|
+
});
|
|
1410
|
+
return createFlowRuntime({ context, executor });
|
|
1411
|
+
} catch {
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
var registration = { contract: FLOW_CONTRACT, mount };
|
|
1416
|
+
if (typeof window !== "undefined") {
|
|
1417
|
+
try {
|
|
1418
|
+
;
|
|
1419
|
+
window[FLOW_GLOBAL] = registration;
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
}
|