@vijaypjavvadi/bdd2pw 2.0.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +207 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/emitters/facade.js +35 -13
- package/dist/emitters/facade.js.map +1 -1
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/anthropicClient.d.ts +2 -0
- package/dist/llm/anthropicClient.js +28 -2
- package/dist/llm/anthropicClient.js.map +1 -1
- package/dist/llm/governanceClient.d.ts +2 -1
- package/dist/llm/governanceClient.js +13 -2
- package/dist/llm/governanceClient.js.map +1 -1
- package/dist/llm/llmStepMatcher.d.ts +11 -0
- package/dist/llm/llmStepMatcher.js +26 -1
- package/dist/llm/llmStepMatcher.js.map +1 -1
- package/dist/llm/types.d.ts +33 -1
- package/dist/transformers/stepMatcher.js +15 -2
- package/dist/transformers/stepMatcher.js.map +1 -1
- package/dist/transformers/stepNormalizer.d.ts +36 -0
- package/dist/transformers/stepNormalizer.js +153 -0
- package/dist/transformers/stepNormalizer.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
- package/templates/heal.ts.tmpl +333 -59
package/templates/heal.ts.tmpl
CHANGED
|
@@ -1,64 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-healing locator helper — v1.1 scaffold.
|
|
2
|
+
* Self-healing locator helper — v1.2.1 scaffold (bdd2pw 2.2.1+).
|
|
3
3
|
*
|
|
4
4
|
* GENERATED FILE. Do not edit directly; this file is rewritten on every
|
|
5
5
|
* `bdd2pw scaffold --self-healing` invocation. To customise, fork it under
|
|
6
6
|
* a different filename and update the `tsconfig.json` `paths` mapping.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* a best-effort POST — failures are silently swallowed so a missing
|
|
17
|
-
* service doesn't break tests.
|
|
8
|
+
* v1.2 → v1.2.1: action-time healing is now applied via INSTANCE
|
|
9
|
+
* MUTATION instead of a Proxy wrapper. v1.2's Proxy broke Playwright
|
|
10
|
+
* `expect(loc).toBeVisible()` because the matcher does a private-field
|
|
11
|
+
* probe (`#frame`) that doesn't survive a generic Proxy — every spec
|
|
12
|
+
* that called expect() on a POM field died synchronously with
|
|
13
|
+
* `Error: toBeVisible can be only used with Locator object`. v1.2.1
|
|
14
|
+
* preserves Locator identity so expect() works normally while keeping
|
|
15
|
+
* the action-time heal-and-retry behaviour intact.
|
|
18
16
|
*
|
|
19
|
-
* What this
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* .
|
|
26
|
-
* .
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
17
|
+
* What this provides today (v1.2.1)
|
|
18
|
+
* ----------------------------------
|
|
19
|
+
* - `healOrThrow(page, { preferred, context })` — registers the locator
|
|
20
|
+
* to `artefacts/heal-events.jsonl` AND mutates it in place so every
|
|
21
|
+
* healable action method (.click, .fill, .check,
|
|
22
|
+
* .selectOption, .hover, .focus, .textContent, .innerText,
|
|
23
|
+
* .getAttribute, .isVisible, .waitFor, .press, .type, .setChecked,
|
|
24
|
+
* .uncheck, .clear, .scrollIntoViewIfNeeded). On the FIRST action
|
|
25
|
+
* failure for a given locator, the proxy:
|
|
26
|
+
* 1. Logs a `heal_attempt` event with the original error.
|
|
27
|
+
* 2. Asks the self-healing service (when SELF_HEALING_URL is set)
|
|
28
|
+
* for a candidate replacement selector via POST /api/v1/heal.
|
|
29
|
+
* 3. Re-runs the original action against `page.locator(<candidate>)`.
|
|
30
|
+
* 4. On success, logs a `healed` event; subsequent actions on the
|
|
31
|
+
* SAME locator instance use the candidate transparently.
|
|
32
|
+
* 5. On failure (no service, no candidate, or candidate also fails),
|
|
33
|
+
* logs `heal_unavailable` and re-throws the original error so
|
|
34
|
+
* the test still fails honestly.
|
|
35
|
+
* - Optional fire-and-forget heartbeat to `${SELF_HEALING_URL}/api/v1/register`
|
|
36
|
+
* so the service knows which locators a test references. Best-effort
|
|
37
|
+
* POST — failures are silently swallowed.
|
|
31
38
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* Action-time healing intentionally fires only on ACTION failures (click,
|
|
40
|
+
* fill, etc.), not on locator construction — `page.getByLabel("Username")`
|
|
41
|
+
* is just an object factory and never throws. The proxy adds zero overhead
|
|
42
|
+
* on the happy path: every method call passes through unmodified, the
|
|
43
|
+
* try/catch only kicks in when the underlying Playwright call rejects.
|
|
37
44
|
*
|
|
38
45
|
* Environment variables
|
|
39
46
|
* ---------------------
|
|
40
|
-
* SELF_HEALING_URL Base URL of the self-healing service
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* JSONL.
|
|
46
|
-
*
|
|
47
|
-
* HEAL_EVENTS_PATH Override the JSONL output path. Defaults to
|
|
48
|
-
* `artefacts/heal-events.jsonl` relative to the
|
|
49
|
-
* process cwd (typically the repo root when running
|
|
50
|
-
* `npx playwright test`).
|
|
51
|
-
*
|
|
52
|
-
* HEAL_DISABLE Set to "1" or "true" to disable both JSONL writes
|
|
53
|
-
* and heartbeats. Useful in CI when you don't want
|
|
54
|
-
* the artefact noise.
|
|
47
|
+
* SELF_HEALING_URL Base URL of the self-healing service.
|
|
48
|
+
* HEAL_EVENTS_PATH Override the JSONL output path (default
|
|
49
|
+
* `artefacts/heal-events.jsonl`).
|
|
50
|
+
* HEAL_DISABLE "1"/"true" disables JSONL writes + heartbeats +
|
|
51
|
+
* heal-service calls. Useful in CI.
|
|
55
52
|
*/
|
|
56
53
|
|
|
57
54
|
import * as fs from "fs";
|
|
58
55
|
import * as path from "path";
|
|
59
56
|
import * as http from "http";
|
|
60
57
|
import * as https from "https";
|
|
61
|
-
import type
|
|
58
|
+
import { test, type Locator, type Page } from "@playwright/test";
|
|
62
59
|
|
|
63
60
|
export interface HealContext {
|
|
64
61
|
/** PascalCase Page Object class name, e.g. "LoginPage". */
|
|
@@ -79,6 +76,27 @@ interface HealEvent {
|
|
|
79
76
|
event: "register";
|
|
80
77
|
page: string;
|
|
81
78
|
name: string;
|
|
79
|
+
/** Scenario / test title at the time the locator was registered. */
|
|
80
|
+
scenario_name: string | null;
|
|
81
|
+
pid: number;
|
|
82
|
+
cwd: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface HealAttemptEvent {
|
|
86
|
+
ts: string;
|
|
87
|
+
event: "heal_attempt" | "healed" | "heal_unavailable";
|
|
88
|
+
page: string;
|
|
89
|
+
name: string;
|
|
90
|
+
scenario_name: string | null;
|
|
91
|
+
method: string;
|
|
92
|
+
/** Original locator selector (best-effort serialisation). */
|
|
93
|
+
original: string | null;
|
|
94
|
+
/** Replacement selector when known. */
|
|
95
|
+
healed: string | null;
|
|
96
|
+
/** Confidence in the replacement, 0–1, when the service provided one. */
|
|
97
|
+
confidence: number | null;
|
|
98
|
+
/** Truncated error message that triggered the heal attempt. */
|
|
99
|
+
error: string | null;
|
|
82
100
|
pid: number;
|
|
83
101
|
cwd: string;
|
|
84
102
|
}
|
|
@@ -103,7 +121,7 @@ function ensureArtefactsDir(): string {
|
|
|
103
121
|
return fullPath;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
function appendEvent(event: HealEvent): void {
|
|
124
|
+
function appendEvent(event: HealEvent | HealAttemptEvent): void {
|
|
107
125
|
if (DISABLED) return;
|
|
108
126
|
const fullPath = ensureArtefactsDir();
|
|
109
127
|
try {
|
|
@@ -113,6 +131,20 @@ function appendEvent(event: HealEvent): void {
|
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Best-effort scenario-name capture. test.info() throws when called
|
|
136
|
+
* outside a running test (`Test info is not available outside the test
|
|
137
|
+
* lifecycle`), so we swallow and return null.
|
|
138
|
+
*/
|
|
139
|
+
function currentScenarioName(): string | null {
|
|
140
|
+
try {
|
|
141
|
+
const info = test.info();
|
|
142
|
+
return info?.title ?? null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
116
148
|
function heartbeat(context: HealContext): void {
|
|
117
149
|
if (DISABLED || !SERVICE_URL) return;
|
|
118
150
|
let url: URL;
|
|
@@ -151,28 +183,270 @@ function heartbeat(context: HealContext): void {
|
|
|
151
183
|
}
|
|
152
184
|
|
|
153
185
|
/**
|
|
154
|
-
* Register a locator with the self-healing pipeline and
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* this.usernameInput = healOrThrow(page, {
|
|
159
|
-
* preferred: page.getByLabel("Username"),
|
|
160
|
-
* context: { page: "LoginPage", name: "usernameInput" },
|
|
161
|
-
* });
|
|
162
|
-
*
|
|
163
|
-
* v1.1 returns `preferred` as-is — no wrapping, no proxy, no action-time
|
|
164
|
-
* healing. The locator is registered to JSONL + heartbeat so the offline
|
|
165
|
-
* self-heal pipeline sees it. v1.2 will wrap action methods.
|
|
186
|
+
* Register a locator with the self-healing pipeline and mutate it in
|
|
187
|
+
* place so action methods perform action-time healing on failure
|
|
188
|
+
* (v1.2.1). Returns the SAME Locator instance — identity is preserved,
|
|
189
|
+
* so `expect(returned).toBeVisible()` works normally.
|
|
166
190
|
*/
|
|
167
|
-
export function healOrThrow(
|
|
191
|
+
export function healOrThrow(page: Page, opts: HealOrThrowOptions): Locator {
|
|
168
192
|
appendEvent({
|
|
169
193
|
ts: new Date().toISOString(),
|
|
170
194
|
event: "register",
|
|
171
195
|
page: opts.context.page,
|
|
172
196
|
name: opts.context.name,
|
|
197
|
+
scenario_name: currentScenarioName(),
|
|
173
198
|
pid: process.pid,
|
|
174
199
|
cwd: process.cwd(),
|
|
175
200
|
});
|
|
176
201
|
heartbeat(opts.context);
|
|
177
|
-
return opts.preferred;
|
|
202
|
+
return wrapLocatorForHealing(page, opts.preferred, opts.context);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ─── v1.2.1 action-time healing (instance mutation, no Proxy) ────── */
|
|
206
|
+
|
|
207
|
+
/** Healable Locator action methods — i.e. ones that throw on element-not-
|
|
208
|
+
* found / timeout. Read methods like .count() / .all() return 0 / [] and
|
|
209
|
+
* don't need wrapping. */
|
|
210
|
+
const HEALABLE_METHODS = new Set<string>([
|
|
211
|
+
"click", "dblclick", "fill", "type", "press", "check", "uncheck",
|
|
212
|
+
"setChecked", "selectOption", "hover", "focus", "blur", "tap",
|
|
213
|
+
"clear", "scrollIntoViewIfNeeded", "waitFor",
|
|
214
|
+
"textContent", "innerText", "innerHTML", "getAttribute", "inputValue",
|
|
215
|
+
"isVisible", "isHidden", "isEnabled", "isDisabled", "isChecked",
|
|
216
|
+
"isEditable",
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
/** Pull a human-readable selector string out of a Locator. Playwright
|
|
220
|
+
* doesn't expose this officially; read its private `_selector` field
|
|
221
|
+
* when present and fall back to `String(locator)`. */
|
|
222
|
+
function locatorSelector(loc: Locator): string | null {
|
|
223
|
+
try {
|
|
224
|
+
const internal = (loc as unknown as { _selector?: string })._selector;
|
|
225
|
+
if (typeof internal === "string" && internal.length > 0) return internal;
|
|
226
|
+
const str = String(loc);
|
|
227
|
+
return str === "[object Object]" ? null : str;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface HealCandidate {
|
|
234
|
+
selector: string;
|
|
235
|
+
confidence: number | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** POST /api/v1/heal — returns a candidate replacement selector or null. */
|
|
239
|
+
async function fetchHealCandidate(
|
|
240
|
+
context: HealContext,
|
|
241
|
+
originalSelector: string | null,
|
|
242
|
+
errorMessage: string,
|
|
243
|
+
): Promise<HealCandidate | null> {
|
|
244
|
+
if (DISABLED || !SERVICE_URL) return null;
|
|
245
|
+
let url: URL;
|
|
246
|
+
try {
|
|
247
|
+
url = new URL("/api/v1/heal", SERVICE_URL);
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const payload = JSON.stringify({
|
|
252
|
+
page: context.page,
|
|
253
|
+
name: context.name,
|
|
254
|
+
original: originalSelector,
|
|
255
|
+
error: errorMessage.slice(0, 500),
|
|
256
|
+
scenario_name: currentScenarioName(),
|
|
257
|
+
});
|
|
258
|
+
const client = url.protocol === "https:" ? https : http;
|
|
259
|
+
return new Promise<HealCandidate | null>((resolve) => {
|
|
260
|
+
let resolved = false;
|
|
261
|
+
const settle = (v: HealCandidate | null) => {
|
|
262
|
+
if (!resolved) { resolved = true; resolve(v); }
|
|
263
|
+
};
|
|
264
|
+
try {
|
|
265
|
+
const req = client.request(
|
|
266
|
+
url,
|
|
267
|
+
{
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: {
|
|
270
|
+
"Content-Type": "application/json",
|
|
271
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
272
|
+
},
|
|
273
|
+
timeout: 5000,
|
|
274
|
+
},
|
|
275
|
+
(res) => {
|
|
276
|
+
const chunks: Buffer[] = [];
|
|
277
|
+
res.on("data", (c) => chunks.push(c));
|
|
278
|
+
res.on("end", () => {
|
|
279
|
+
try {
|
|
280
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
281
|
+
const parsed = JSON.parse(body) as { selector?: string; confidence?: number };
|
|
282
|
+
if (parsed && typeof parsed.selector === "string") {
|
|
283
|
+
settle({ selector: parsed.selector, confidence: typeof parsed.confidence === "number" ? parsed.confidence : null });
|
|
284
|
+
} else {
|
|
285
|
+
settle(null);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
settle(null);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
res.on("error", () => settle(null));
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
req.on("error", () => settle(null));
|
|
295
|
+
req.on("timeout", () => { req.destroy(); settle(null); });
|
|
296
|
+
req.write(payload);
|
|
297
|
+
req.end();
|
|
298
|
+
} catch {
|
|
299
|
+
settle(null);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Add action-time healing to a Locator by MUTATING IT IN PLACE.
|
|
306
|
+
*
|
|
307
|
+
* v1.2 used a `new Proxy(...)` wrapper here, which broke any spec that
|
|
308
|
+
* called `await expect(loc).toBeVisible()` — Playwright's matchers do
|
|
309
|
+
* a private-field probe (`#frame` and friends) that doesn't survive a
|
|
310
|
+
* generic Proxy. Result: 100% test fail rate on any spec that asserts
|
|
311
|
+
* against a POM field, with the unhelpful error
|
|
312
|
+
* `Error: toBeVisible can be only used with Locator object`.
|
|
313
|
+
*
|
|
314
|
+
* v1.2.1 replaces the Proxy with direct method override:
|
|
315
|
+
* - For every healable method (click, fill, …), capture the original
|
|
316
|
+
* bound function via closure BEFORE mutation.
|
|
317
|
+
* - Replace the property on the Locator INSTANCE with a wrapper that
|
|
318
|
+
* try/catches and heals on failure.
|
|
319
|
+
* - Return the original Locator unchanged in identity. `instanceof
|
|
320
|
+
* Locator` still holds; private-field probes still work; `expect()`
|
|
321
|
+
* accepts it normally; non-healable methods (.locator(), .first(),
|
|
322
|
+
* etc.) keep their prototype's implementation untouched.
|
|
323
|
+
*
|
|
324
|
+
* After a successful heal, `healedLocator` is set in the closure and
|
|
325
|
+
* subsequent calls (on any healable method on this same instance) go
|
|
326
|
+
* through it transparently. Cross-method healing of one cached
|
|
327
|
+
* candidate, exactly as v1.2 promised — but without sacrificing
|
|
328
|
+
* matcher compatibility.
|
|
329
|
+
*/
|
|
330
|
+
function wrapLocatorForHealing(page: Page, original: Locator, context: HealContext): Locator {
|
|
331
|
+
let healedLocator: Locator | null = null;
|
|
332
|
+
|
|
333
|
+
for (const method of HEALABLE_METHODS) {
|
|
334
|
+
// Capture the original method BEFORE mutation — otherwise we'd
|
|
335
|
+
// recurse infinitely when our wrapper tries to call the "original".
|
|
336
|
+
const origFn = (original as unknown as Record<string, unknown>)[method];
|
|
337
|
+
if (typeof origFn !== "function") continue;
|
|
338
|
+
|
|
339
|
+
(original as unknown as Record<string, unknown>)[method] = async function (
|
|
340
|
+
...args: unknown[]
|
|
341
|
+
) {
|
|
342
|
+
// Once healed, route every healable call through the candidate.
|
|
343
|
+
if (healedLocator) {
|
|
344
|
+
const fn = (healedLocator as unknown as Record<string, Function>)[method];
|
|
345
|
+
return await fn.apply(healedLocator, args);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
return await (origFn as (...a: unknown[]) => Promise<unknown>).apply(
|
|
350
|
+
original,
|
|
351
|
+
args,
|
|
352
|
+
);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
355
|
+
const original_selector = locatorSelector(original);
|
|
356
|
+
appendEvent({
|
|
357
|
+
ts: new Date().toISOString(),
|
|
358
|
+
event: "heal_attempt",
|
|
359
|
+
page: context.page,
|
|
360
|
+
name: context.name,
|
|
361
|
+
scenario_name: currentScenarioName(),
|
|
362
|
+
method,
|
|
363
|
+
original: original_selector,
|
|
364
|
+
healed: null,
|
|
365
|
+
confidence: null,
|
|
366
|
+
error: errMsg.slice(0, 500),
|
|
367
|
+
pid: process.pid,
|
|
368
|
+
cwd: process.cwd(),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const candidate = await fetchHealCandidate(context, original_selector, errMsg);
|
|
372
|
+
if (!candidate) {
|
|
373
|
+
appendEvent({
|
|
374
|
+
ts: new Date().toISOString(),
|
|
375
|
+
event: "heal_unavailable",
|
|
376
|
+
page: context.page,
|
|
377
|
+
name: context.name,
|
|
378
|
+
scenario_name: currentScenarioName(),
|
|
379
|
+
method,
|
|
380
|
+
original: original_selector,
|
|
381
|
+
healed: null,
|
|
382
|
+
confidence: null,
|
|
383
|
+
error: errMsg.slice(0, 500),
|
|
384
|
+
pid: process.pid,
|
|
385
|
+
cwd: process.cwd(),
|
|
386
|
+
});
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let candidateLocator: Locator;
|
|
391
|
+
try {
|
|
392
|
+
candidateLocator = page.locator(candidate.selector);
|
|
393
|
+
} catch {
|
|
394
|
+
appendEvent({
|
|
395
|
+
ts: new Date().toISOString(),
|
|
396
|
+
event: "heal_unavailable",
|
|
397
|
+
page: context.page,
|
|
398
|
+
name: context.name,
|
|
399
|
+
scenario_name: currentScenarioName(),
|
|
400
|
+
method,
|
|
401
|
+
original: original_selector,
|
|
402
|
+
healed: candidate.selector,
|
|
403
|
+
confidence: candidate.confidence,
|
|
404
|
+
error: "candidate selector failed to construct",
|
|
405
|
+
pid: process.pid,
|
|
406
|
+
cwd: process.cwd(),
|
|
407
|
+
});
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const retryFn = (candidateLocator as unknown as Record<string, Function>)[method];
|
|
413
|
+
const retryResult = await retryFn.apply(candidateLocator, args);
|
|
414
|
+
healedLocator = candidateLocator;
|
|
415
|
+
appendEvent({
|
|
416
|
+
ts: new Date().toISOString(),
|
|
417
|
+
event: "healed",
|
|
418
|
+
page: context.page,
|
|
419
|
+
name: context.name,
|
|
420
|
+
scenario_name: currentScenarioName(),
|
|
421
|
+
method,
|
|
422
|
+
original: original_selector,
|
|
423
|
+
healed: candidate.selector,
|
|
424
|
+
confidence: candidate.confidence,
|
|
425
|
+
error: errMsg.slice(0, 500),
|
|
426
|
+
pid: process.pid,
|
|
427
|
+
cwd: process.cwd(),
|
|
428
|
+
});
|
|
429
|
+
return retryResult;
|
|
430
|
+
} catch (retryErr) {
|
|
431
|
+
appendEvent({
|
|
432
|
+
ts: new Date().toISOString(),
|
|
433
|
+
event: "heal_unavailable",
|
|
434
|
+
page: context.page,
|
|
435
|
+
name: context.name,
|
|
436
|
+
scenario_name: currentScenarioName(),
|
|
437
|
+
method,
|
|
438
|
+
original: original_selector,
|
|
439
|
+
healed: candidate.selector,
|
|
440
|
+
confidence: candidate.confidence,
|
|
441
|
+
error: retryErr instanceof Error ? retryErr.message.slice(0, 500) : String(retryErr).slice(0, 500),
|
|
442
|
+
pid: process.pid,
|
|
443
|
+
cwd: process.cwd(),
|
|
444
|
+
});
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return original;
|
|
178
452
|
}
|