sela-core 0.1.0-alpha.41 → 0.1.0-alpha.42

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.
@@ -1,12 +1,36 @@
1
- import { Locator } from "@playwright/test";
1
+ import { expect as playwrightExpect, Locator } from "@playwright/test";
2
2
  import { SelaEngine } from "../engine/SelaEngine";
3
+ import { SelectorContext } from "../engine/HealingRegistry";
4
+ type PlaywrightExpect = typeof playwrightExpect;
3
5
  /**
4
- * createHealingExpect - drop-in replacement for Playwright's expect().
6
+ * Resolution returned for each `expect(value)` call: the page the value lives
7
+ * on (or null when there is none / it is not a locator), the heal context
8
+ * (file + line), and the unwrapped live Locator behind a Sela proxy (if any).
9
+ */
10
+ export interface ExpectResolution {
11
+ page: import("@playwright/test").Page | null;
12
+ ctx: SelectorContext;
13
+ locator: Locator | null;
14
+ }
15
+ export type ExpectResolver = (value: unknown) => ExpectResolution;
16
+ /**
17
+ * The unified healing-expect factory. Returns a Proxy whose target is the real
18
+ * Playwright `expect` (or a `configure()`-d variant), so it is structurally
19
+ * indistinguishable from Playwright's `Expect` — same call signature, same
20
+ * static surface — with healing layered on locator matchers.
21
+ */
22
+ export declare function makeHealingExpect(engine: SelaEngine, resolve: ExpectResolver, baseExpect?: PlaywrightExpect): PlaywrightExpect;
23
+ /**
24
+ * createHealingExpect - fixture-scoped healing expect.
25
+ *
26
+ * Page and file are fixed (from the test fixture); the source line is resolved
27
+ * per call from the stack. `resolveProxy` unwraps a Sela locator proxy to its
28
+ * current live Locator (supplied by fixtures/index.ts which owns
29
+ * proxyToActiveLocator).
5
30
  *
6
- * @param resolveProxy Optional callback that unwraps a Sela proxy
7
- * to its current live Locator. Supplied by
8
- * fixtures/index.ts which owns proxyToActiveLocator.
31
+ * Signature preserved for backward compatibility; internally delegates to the
32
+ * unified makeHealingExpect so the export is full Playwright `Expect` parity.
9
33
  */
10
- export declare function createHealingExpect(engine: SelaEngine, page: import("@playwright/test").Page, filePath: string, initialLine: number, // השורה מהפיקסצ'ר (לרוב תהיה לא מדויקת או 0)
11
- resolveProxy?: (value: unknown) => Locator | null): (locatorOrValue: unknown) => any;
34
+ export declare function createHealingExpect(engine: SelaEngine, page: import("@playwright/test").Page, filePath: string, initialLine: number, resolveProxy?: (value: unknown) => Locator | null): PlaywrightExpect;
35
+ export {};
12
36
  //# sourceMappingURL=expectProxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"expectProxy.d.ts","sourceRoot":"","sources":["../../src/fixtures/expectProxy.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,OAAO,EAER,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAkOlD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,OAAO,kBAAkB,EAAE,IAAI,EACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,6CAA6C;AAClE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,GAChD,CAAC,cAAc,EAAE,OAAO,KAAK,GAAG,CAsElC"}
1
+ {"version":3,"file":"expectProxy.d.ts","sourceRoot":"","sources":["../../src/fixtures/expectProxy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAmB,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAI7E,KAAK,gBAAgB,GAAG,OAAO,gBAAgB,CAAC;AAmVhD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,kBAAkB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7C,GAAG,EAAE,eAAe,CAAC;IACrB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,gBAAgB,CAAC;AAElE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,cAAc,EACvB,UAAU,GAAE,gBAAmC,GAC9C,gBAAgB,CA+BlB;AA0DD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,OAAO,kBAAkB,EAAE,IAAI,EACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,GAChD,gBAAgB,CAOlB"}
@@ -1,8 +1,26 @@
1
1
  "use strict";
2
2
  // src/fixtures/expectProxy.ts - full replacement
3
+ //
4
+ // Drop-in parity with Playwright's `expect`.
5
+ //
6
+ // Instead of REBUILDING expect from scratch (which silently dropped every
7
+ // static — soft/poll/extend/configure/asymmetric matchers — and ignored the
8
+ // 2nd `messageOrOptions` argument), we now INTERCEPT the genuine
9
+ // `playwrightExpect` via a Proxy:
10
+ //
11
+ // • apply trap → forwards `(actual, ...rest)` verbatim through
12
+ // Reflect.apply, then layers healing on locator matchers.
13
+ // • get trap → forwards every static (poll/extend/getState/not/<asym>)
14
+ // to the real object; only `soft`/`configure` are wrapped so
15
+ // they keep healing + return Sela-tagged expects.
16
+ //
17
+ // Forwarding is the default; healing is the exception. Anything Playwright
18
+ // adds in a future version is reachable for free.
3
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.makeHealingExpect = makeHealingExpect;
4
21
  exports.createHealingExpect = createHealingExpect;
5
22
  const test_1 = require("@playwright/test");
23
+ const crypto_1 = require("crypto");
6
24
  const HealingRegistry_1 = require("../engine/HealingRegistry");
7
25
  const proxyTag_1 = require("./proxyTag");
8
26
  const logger_1 = require("../utils/logger");
@@ -42,16 +60,6 @@ function isRealLocator(value) {
42
60
  value._impl?._frameSelector !== undefined;
43
61
  return hasFrame && !isFrameLocator;
44
62
  }
45
- /**
46
- * Detect whether a locator is frame-scoped by checking its internal
47
- * selector string for frame-related segments.
48
- */
49
- function isFrameScopedLocator(locator) {
50
- const sel = locator._selector ?? locator["_impl"]?._selector ?? "";
51
- return (typeof sel === "string" &&
52
- (sel.includes("internal:control=enter-frame") ||
53
- sel.includes(">> internal:control=enter-frame")));
54
- }
55
63
  /**
56
64
  * Extracts the internal Playwright selector string from a real Locator.
57
65
  */
@@ -88,8 +96,7 @@ function extractSelectorFromLocator(locator) {
88
96
  * → page.locator("#simple-element")
89
97
  */
90
98
  function rebuildLocator(page, healedSelector) {
91
- // --- הוספת השורה הזו ---
92
- // מוודא שאנחנו עובדים עם האובייקט האמיתי של פליירייט ולא עם הפרוקסי שלנו
99
+ // Always operate on the genuine Playwright page, never the Sela page proxy.
93
100
  const rawPage = page._rawPage || page;
94
101
  const segments = healedSelector
95
102
  .split(" >> ")
@@ -110,137 +117,263 @@ function rebuildLocator(page, healedSelector) {
110
117
  context = context.frameLocator(frameSel);
111
118
  }
112
119
  }
113
- // כאן אנחנו מחזירים לוקטור שנוצר ישירות מה-rawPage
114
120
  return context.locator(elementSegment);
115
121
  }
116
122
  /**
117
- * Wrap a single matcher so that element-not-found errors trigger the
118
- * heal cycle and retry the assertion once on the correctly rebuilt locator.
123
+ * Resolve a healed selector for a failing locator, going through the
124
+ * HealingRegistry fast-path first and the engine + LLM slow-path second.
125
+ *
126
+ * Returns the canonical healed selector string, or `null` when no heal is
127
+ * available (selector unchanged / engine declined). A `[SafetyGuard]` block is
128
+ * re-thrown so the caller surfaces the protected-element message - Sela must
129
+ * never silently swallow a deliberate refusal-to-heal.
130
+ */
131
+ async function healSelector(locator, ctx, engine, page) {
132
+ const rawSelector = extractSelectorFromLocator(locator);
133
+ if (!rawSelector)
134
+ return null;
135
+ logger_1.logger.debug(`Assertion failed for "${rawSelector}", attempting heal…`);
136
+ // Fast path - already healed in this test via the registry.
137
+ const resolved = HealingRegistry_1.HealingRegistry.getInstance().resolveSelector(rawSelector, ctx);
138
+ if (resolved !== rawSelector)
139
+ return resolved;
140
+ // Slow path - ask the engine + LLM. engine.heal() writes the fix to disk
141
+ // AND returns the canonical healedLocatorString from SourceUpdater - the
142
+ // exact string saved to the file. This handshake keeps disk and runtime in
143
+ // sync (no "ghost failures").
144
+ const stableId = (0, crypto_1.createHash)("md5").update(rawSelector).digest("hex");
145
+ const healed = await engine.heal(page, rawSelector, rawSelector, stableId, ctx.filePath, ctx.line, "assertion");
146
+ if (!healed || healed === rawSelector)
147
+ return null;
148
+ return healed;
149
+ }
150
+ const WRAPPED_MATCHERS = [
151
+ "toBeVisible",
152
+ "toBeHidden",
153
+ "toBeEnabled",
154
+ "toBeDisabled",
155
+ "toHaveText",
156
+ "toContainText",
157
+ "toHaveValue",
158
+ "toHaveAttribute",
159
+ "toHaveCount",
160
+ "toBeChecked",
161
+ "toBeFocused",
162
+ ];
163
+ /** Build an expectation for the (possibly negated) matcher chain. */
164
+ function expectationFor(base, locator, negated) {
165
+ const e = base(locator);
166
+ return negated ? e.not : e;
167
+ }
168
+ /**
169
+ * Wrap a HARD matcher so element-not-found errors trigger the heal cycle and
170
+ * retry the assertion once on the correctly rebuilt locator.
119
171
  */
120
- function wrapMatcher(matcher, locator, matcherName, engine, page, ctx) {
121
- return (async (...args) => {
172
+ function wrapHardMatcher(matcher, locator, matcherName, engine, page, ctx, opts) {
173
+ return async (...args) => {
122
174
  try {
123
175
  return await matcher(...args);
124
176
  }
125
177
  catch (err) {
126
178
  if (!isElementNotFoundError(err))
127
179
  throw err;
128
- const rawSelector = extractSelectorFromLocator(locator);
129
- if (!rawSelector)
180
+ let healed;
181
+ try {
182
+ healed = await healSelector(locator, ctx, engine, page);
183
+ }
184
+ catch (healErr) {
185
+ // [SafetyGuard] blocks are deliberate refusals — surface them.
186
+ if (healErr instanceof Error &&
187
+ healErr.message.includes("[SafetyGuard]")) {
188
+ throw healErr;
189
+ }
190
+ throw err;
191
+ }
192
+ if (!healed)
193
+ throw err;
194
+ const newLocator = rebuildLocator(page, healed);
195
+ const retry = expectationFor(opts.baseExpect, newLocator, opts.negated);
196
+ const retryMatcher = retry[matcherName];
197
+ if (typeof retryMatcher !== "function")
130
198
  throw err;
131
- logger_1.logger.debug(`Assertion failed for "${rawSelector}", attempting heal…`);
132
- // Fast path - already healed in this test via registry
133
- const resolved = HealingRegistry_1.HealingRegistry.getInstance().resolveSelector(rawSelector, ctx);
134
- let healedSelector = resolved;
135
- if (resolved === rawSelector) {
136
- // Slow path - ask the engine + LLM.
137
- // engine.heal() writes the fix to disk AND returns the canonical
138
- // healedLocatorString from SourceUpdater - the exact same string
139
- // that was saved to the file. This is the handshake that guarantees
140
- // disk and runtime are in sync (no "ghost failures").
141
- const stableId = require("crypto")
142
- .createHash("md5")
143
- .update(rawSelector)
144
- .digest("hex");
199
+ logger_1.logger.debug(`Retrying assertion with healed selector: "${healed}"`);
200
+ return await retryMatcher.apply(retry, args);
201
+ }
202
+ };
203
+ }
204
+ /**
205
+ * Wrap a SOFT matcher.
206
+ *
207
+ * Soft assertions never throw Playwright records the failure on testInfo and
208
+ * resolves so the heal-on-throw path used for hard matchers cannot observe a
209
+ * failure. We therefore probe HARD first to surface element-not-found, heal if
210
+ * needed, then run the authoritative SOFT assertion on the (possibly healed)
211
+ * locator so the result is recorded with correct soft semantics.
212
+ *
213
+ * Trade-off: a soft assertion that fails for a NON-stale reason (element
214
+ * present, condition false) waits the matcher timeout twice (hard probe + soft
215
+ * record). This only affects the already-slow genuine-failure path; the pass
216
+ * path and the stale-selector path each run the matcher effectively once.
217
+ */
218
+ function wrapSoftMatcher(locator, matcherName, engine, page, ctx, opts) {
219
+ return async (...args) => {
220
+ let target = locator;
221
+ try {
222
+ const hard = expectationFor(opts.baseExpect, target, opts.negated);
223
+ await hard[matcherName](...args);
224
+ return; // passed hard ⇒ passes soft, nothing to record
225
+ }
226
+ catch (err) {
227
+ if (isElementNotFoundError(err)) {
228
+ let healed = null;
145
229
  try {
146
- healedSelector = await engine.heal(page, rawSelector, rawSelector, stableId, ctx.filePath, ctx.line, "assertion");
230
+ healed = await healSelector(locator, ctx, engine, page);
147
231
  }
148
232
  catch (healErr) {
149
233
  if (healErr instanceof Error &&
150
234
  healErr.message.includes("[SafetyGuard]")) {
151
235
  throw healErr;
152
236
  }
153
- throw err;
237
+ }
238
+ if (healed) {
239
+ target = rebuildLocator(page, healed);
240
+ logger_1.logger.debug(`Soft assertion healed to selector: "${healed}"`);
154
241
  }
155
242
  }
156
- if (!healedSelector || healedSelector === rawSelector)
157
- throw err;
158
- // Rebuild the locator from the canonical selector returned by the
159
- // updater. rebuildLocator() correctly chains frameLocator() calls
160
- // for each frame segment, then .locator() for the final element,
161
- // ensuring page/frame/locator variables are scoped correctly at runtime.
162
- const newLocator = rebuildLocator(page, healedSelector);
163
- const newExpectation = (0, test_1.expect)(newLocator);
164
- const retryMatcher = newExpectation[matcherName];
165
- if (typeof retryMatcher !== "function")
166
- throw err;
167
- logger_1.logger.debug(`Retrying assertion with healed selector: "${healedSelector}"`);
168
- return await retryMatcher.apply(newExpectation, args);
243
+ // Record softly on the (possibly healed) target.
244
+ const softExp = opts.baseExpect.soft(target);
245
+ const chain = opts.negated ? softExp.not : softExp;
246
+ return await chain[matcherName](...args);
169
247
  }
170
- });
248
+ };
171
249
  }
172
- const WRAPPED_MATCHERS = [
173
- "toBeVisible",
174
- "toBeHidden",
175
- "toBeEnabled",
176
- "toBeDisabled",
177
- "toHaveText",
178
- "toContainText",
179
- "toHaveValue",
180
- "toHaveAttribute",
181
- "toHaveCount",
182
- "toBeChecked",
183
- "toBeFocused",
184
- ];
185
250
  /**
186
- * createHealingExpect - drop-in replacement for Playwright's expect().
187
- *
188
- * @param resolveProxy Optional callback that unwraps a Sela proxy
189
- * to its current live Locator. Supplied by
190
- * fixtures/index.ts which owns proxyToActiveLocator.
251
+ * Wrap a Playwright expectation object so its locator matchers heal, and so the
252
+ * `.not` chain stays wrapped (otherwise `expect(loc).not.toBeVisible()` would
253
+ * lose healing). `.resolves` / `.rejects` are promise chains (non-locator) and
254
+ * pass through untouched.
191
255
  */
192
- function createHealingExpect(engine, page, filePath, initialLine, // השורה מהפיקסצ'ר (לרוב תהיה לא מדויקת או 0)
193
- resolveProxy) {
194
- return function expect(locatorOrValue) {
195
- // 1. חילוץ דינמי של שורת הקוד האמיתית בזמן קריאת ה-expect
196
- const getCallerLine = () => {
197
- const err = new Error();
198
- const stack = err.stack?.split("\n") || [];
199
- // מחפשים את הקריאה הראשונה בשרשרת שמגיעה מקובץ הטסט ולא מקובץ הפרוקסי
200
- const specLine = stack.find((l) => l.includes(filePath) && !l.includes("expectProxy"));
201
- if (specLine) {
202
- const match = specLine.match(/:(\d+):(\d+)/);
203
- return match ? parseInt(match[1], 10) : initialLine;
256
+ function wrapExpectation(base, locator, ctx, engine, page, opts) {
257
+ return new Proxy(base, {
258
+ get(target, prop) {
259
+ // Sela proxy identity honored before any string-only branch.
260
+ if (prop === proxyTag_1.SELA_PROXY)
261
+ return "expect";
262
+ if (typeof prop !== "string")
263
+ return target[prop];
264
+ const val = target[prop];
265
+ // Re-wrap the negated chain so heals propagate through `.not`.
266
+ if (prop === "not" && val && typeof val === "object") {
267
+ return wrapExpectation(val, locator, ctx, engine, page, {
268
+ ...opts,
269
+ negated: !opts.negated,
270
+ });
204
271
  }
205
- return initialLine;
206
- };
207
- const actualLine = getCallerLine(); // כאן אנחנו מקבלים את השורה הנכונה!
208
- // Step 1 - unwrap Sela proxy to real active Locator
209
- let resolved = locatorOrValue;
210
- if (resolveProxy) {
211
- const unwrapped = resolveProxy(locatorOrValue);
212
- if (unwrapped)
213
- resolved = unwrapped;
214
- }
215
- const base = (0, test_1.expect)(resolved);
216
- // Defensive: if for any reason `base` is not an object, return it as-is
217
- // rather than crashing inside `new Proxy(...)`.
218
- if (base === null || typeof base !== "object") {
272
+ if (typeof val === "function" &&
273
+ WRAPPED_MATCHERS.includes(prop)) {
274
+ return opts.soft
275
+ ? wrapSoftMatcher(locator, prop, engine, page, ctx, opts)
276
+ : wrapHardMatcher(val.bind(target), locator, prop, engine, page, ctx, opts);
277
+ }
278
+ return val;
279
+ },
280
+ });
281
+ }
282
+ /**
283
+ * The unified healing-expect factory. Returns a Proxy whose target is the real
284
+ * Playwright `expect` (or a `configure()`-d variant), so it is structurally
285
+ * indistinguishable from Playwright's `Expect` same call signature, same
286
+ * static surface — with healing layered on locator matchers.
287
+ */
288
+ function makeHealingExpect(engine, resolve, baseExpect = test_1.expect) {
289
+ const handler = {
290
+ // expect(actual, messageOrOptions?) — rest args forwarded verbatim.
291
+ apply(target, thisArg, args) {
292
+ const [value, ...rest] = args;
293
+ const { page, ctx, locator } = resolve(value);
294
+ const subject = locator ?? value;
295
+ const base = Reflect.apply(target, thisArg, [subject, ...rest]);
296
+ if (!page)
297
+ return base;
298
+ if (base === null || typeof base !== "object")
299
+ return base;
300
+ if (!isRealLocator(subject))
301
+ return base;
302
+ return wrapExpectation(base, subject, ctx, engine, page, {
303
+ negated: false,
304
+ soft: false,
305
+ baseExpect,
306
+ });
307
+ },
308
+ // Statics: forward everything; wrap only soft + configure.
309
+ get(target, prop, receiver) {
310
+ if (prop === proxyTag_1.SELA_PROXY)
311
+ return "expect";
312
+ if (prop === "soft")
313
+ return makeSoft(engine, resolve, baseExpect);
314
+ if (prop === "configure")
315
+ return makeConfigure(engine, resolve, baseExpect);
316
+ return Reflect.get(target, prop, receiver);
317
+ },
318
+ };
319
+ return new Proxy(baseExpect, handler);
320
+ }
321
+ /** Heal-aware `expect.soft`. Non-locator values fall through to native soft. */
322
+ function makeSoft(engine, resolve, baseExpect) {
323
+ return (value, ...rest) => {
324
+ const { page, ctx, locator } = resolve(value);
325
+ const subject = locator ?? value;
326
+ const base = baseExpect.soft(subject, ...rest);
327
+ if (!page)
219
328
  return base;
220
- }
221
- // Step 2 - only wrap matchers for genuine page-level Locator objects.
222
- if (!isRealLocator(resolved)) {
329
+ if (base === null || typeof base !== "object")
223
330
  return base;
224
- }
225
- const locator = resolved;
226
- // משתמשים בשורה האמיתית שחילצנו עבור ה-Context שמועבר ל-Engine
227
- const ctx = { filePath, line: actualLine };
228
- const proxy = new Proxy(base, {
229
- get(target, prop) {
230
- // Sela proxy identity - honored before any string-only branch so
231
- // callers can detect a Sela expect proxy via Symbol.for("sela.proxy").
232
- if (prop === proxyTag_1.SELA_PROXY)
233
- return "expect";
234
- if (typeof prop !== "string")
235
- return target[prop];
236
- const val = target[prop];
237
- if (typeof val === "function" &&
238
- WRAPPED_MATCHERS.includes(prop)) {
239
- return wrapMatcher(val.bind(target), locator, prop, engine, page, ctx);
240
- }
241
- return val;
242
- },
331
+ if (!isRealLocator(subject))
332
+ return base;
333
+ return wrapExpectation(base, subject, ctx, engine, page, {
334
+ negated: false,
335
+ soft: true,
336
+ baseExpect,
243
337
  });
244
- return proxy;
245
338
  };
246
339
  }
340
+ /**
341
+ * Heal-aware `expect.configure`. Returns a NEW healing expect whose base is the
342
+ * configured Playwright expect, so configured timeouts/soft defaults apply on
343
+ * both the first attempt and the heal retry.
344
+ */
345
+ function makeConfigure(engine, resolve, baseExpect) {
346
+ return (configuration) => {
347
+ const configured = baseExpect.configure(configuration);
348
+ return makeHealingExpect(engine, resolve, configured);
349
+ };
350
+ }
351
+ /** Resolve the caller's source line within `filePath` from the live stack. */
352
+ function getCallerLine(filePath, fallback) {
353
+ const stack = new Error().stack?.split("\n") ?? [];
354
+ const specLine = stack.find((l) => filePath !== "" && l.includes(filePath) && !l.includes("expectProxy"));
355
+ if (specLine) {
356
+ const match = specLine.match(/:(\d+):(\d+)/);
357
+ return match ? parseInt(match[1], 10) : fallback;
358
+ }
359
+ return fallback;
360
+ }
361
+ /**
362
+ * createHealingExpect - fixture-scoped healing expect.
363
+ *
364
+ * Page and file are fixed (from the test fixture); the source line is resolved
365
+ * per call from the stack. `resolveProxy` unwraps a Sela locator proxy to its
366
+ * current live Locator (supplied by fixtures/index.ts which owns
367
+ * proxyToActiveLocator).
368
+ *
369
+ * Signature preserved for backward compatibility; internally delegates to the
370
+ * unified makeHealingExpect so the export is full Playwright `Expect` parity.
371
+ */
372
+ function createHealingExpect(engine, page, filePath, initialLine, resolveProxy) {
373
+ const resolve = (value) => ({
374
+ page,
375
+ locator: resolveProxy ? resolveProxy(value) : null,
376
+ ctx: { filePath, line: getCallerLine(filePath, initialLine) },
377
+ });
378
+ return makeHealingExpect(engine, resolve);
379
+ }
@@ -1,2 +1,3 @@
1
- export declare function expect(value: unknown): any;
1
+ import { type Expect } from "@playwright/test";
2
+ export declare const expect: Expect<{}>;
2
3
  //# sourceMappingURL=moduleExpect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"moduleExpect.d.ts","sourceRoot":"","sources":["../../src/fixtures/moduleExpect.ts"],"names":[],"mappings":"AA8CA,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAa1C"}
1
+ {"version":3,"file":"moduleExpect.d.ts","sourceRoot":"","sources":["../../src/fixtures/moduleExpect.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,MAAM,EACZ,MAAM,kBAAkB,CAAC;AA0D1B,eAAO,MAAM,MAAM,EAGH,MAAM,CAAC,EAAE,CAAC,CAAC"}
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
- // Standalone module-level expect for `import { expect } from 'sela'` usage.
3
- // Creates a per-call healing expect using the shared engine singleton.
4
- // Falls back to raw Playwright expect for non-Locator values.
2
+ // Standalone module-level expect for `import { expect } from 'sela-core'` usage.
3
+ //
4
+ // Full Playwright `Expect` parity: this is the genuine `playwrightExpect`
5
+ // behind a Proxy (see makeHealingExpect), so every static — soft / poll /
6
+ // extend / configure / getState / not / asymmetric matchers — and the 2-arg
7
+ // `(actual, messageOrOptions?)` signature are preserved. Healing is layered on
8
+ // locator matchers using the shared engine singleton; the page is detected
9
+ // per-call from the asserted value, falling back to native Playwright behavior
10
+ // for non-locator values.
5
11
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.expect = expect;
7
- const test_1 = require("@playwright/test");
12
+ exports.expect = void 0;
8
13
  const singleton_js_1 = require("../engine/singleton.js");
9
14
  const expectProxy_js_1 = require("./expectProxy.js");
10
15
  const index_js_1 = require("./index.js");
@@ -26,29 +31,33 @@ function extractPage(value) {
26
31
  }
27
32
  return null;
28
33
  }
29
- function getCallerFile() {
34
+ /**
35
+ * Resolve the caller's spec file + line from the live stack. Matches BOTH frame
36
+ * shapes V8 emits, including Windows drive-letter paths:
37
+ * " at fnName (D:\\path\\file.spec.ts:17:20)"
38
+ * " at D:\\path\\file.spec.ts:17:20"
39
+ */
40
+ function getCallerSite() {
30
41
  const lines = new Error().stack?.split("\n") ?? [];
31
- // Match BOTH frame shapes V8 emits:
32
- // " at fnName (D:\\path\\file.spec.ts:17:20)"
33
- // " at D:\\path\\file.spec.ts:17:20"
34
- // The path may include Windows drive letters (colon after C/D/...).
35
- const FRAME_WITH_PARENS = /\(([^()]+?):\d+:\d+\)\s*$/;
36
- const FRAME_BARE = /\s+at\s+(.+?):\d+:\d+\s*$/;
42
+ const FRAME_WITH_PARENS = /\(([^()]+?):(\d+):\d+\)\s*$/;
43
+ const FRAME_BARE = /\s+at\s+(.+?):(\d+):\d+\s*$/;
37
44
  for (let i = 1; i < lines.length; i++) {
38
45
  const line = lines[i];
39
46
  if (!/\.(spec|test)\.[jt]sx?/.test(line))
40
47
  continue;
41
48
  const m = line.match(FRAME_WITH_PARENS) ?? line.match(FRAME_BARE);
42
- if (m && m[1])
43
- return m[1].trim();
44
- }
45
- return "";
46
- }
47
- function expect(value) {
48
- const page = extractPage(value);
49
- if (page) {
50
- const filePath = getCallerFile();
51
- return (0, expectProxy_js_1.createHealingExpect)(singleton_js_1.sharedEngine, page, filePath, 0, index_js_1.resolveSelaProxy)(value);
49
+ if (m && m[1]) {
50
+ return { filePath: m[1].trim(), line: parseInt(m[2], 10) };
51
+ }
52
52
  }
53
- return (0, test_1.expect)(value);
53
+ return { filePath: "", line: 0 };
54
54
  }
55
+ const resolve = (value) => {
56
+ const { filePath, line } = getCallerSite();
57
+ return {
58
+ page: extractPage(value),
59
+ locator: (0, index_js_1.resolveSelaProxy)(value),
60
+ ctx: { filePath, line },
61
+ };
62
+ };
63
+ exports.expect = (0, expectProxy_js_1.makeHealingExpect)(singleton_js_1.sharedEngine, resolve);
package/dist/index.cjs ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ // Playwright-identical callable module root.
3
+ //
4
+ // Mirrors `node_modules/playwright/test.js`:
5
+ // module.exports = Object.assign(test, combinedExports)
6
+ //
7
+ // The module IS the `test` function, with every other export hung off it, so
8
+ // ALL of the following resolve to the same object/contract:
9
+ // require('sela-core') → callable `test`
10
+ // require('sela-core').expect → expect
11
+ // require('sela-core').default → test
12
+ // import test from 'sela-core' → test (via .default interop)
13
+ // import { test, expect } ... → named
14
+ //
15
+ // `package.json` maps "require" → this file (dist/index.cjs) and "import" →
16
+ // the tsc-emitted ESM-facing module (dist/index.js, static named exports).
17
+ // Types for both come from dist/index.d.ts.
18
+ // `./index.js` is the compiled src/index.ts (CommonJS): carries `test`,
19
+ // `expect`, value re-exports, `defineConfig`, `SelaReporter`, `__esModule`,
20
+ // and `default` (= test).
21
+ const api = require("./index.js");
22
+ const root = Object.assign(api.test, api);
23
+ // Self-consistent default: api.default is already `test`; pin it explicitly so
24
+ // the callable root and its `.default` are the same function.
25
+ root.default = api.test;
26
+ module.exports = root;
@@ -0,0 +1,3 @@
1
+ declare const root: any;
2
+ export = root;
3
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../src/index.cts"],"names":[],"mappings":"AAsBA,QAAA,MAAM,IAAI,EAAE,GAAkC,CAAC;AAK/C,SAAS,IAAI,CAAC"}
package/dist/index.d.ts CHANGED
@@ -2,7 +2,9 @@ export { defineConfig } from "./config/defineConfig.js";
2
2
  export type { SelaConfig, ResolvedConfig } from "./config/SelaConfig.js";
3
3
  export type * from "@playwright/test";
4
4
  export { chromium, firefox, webkit, devices, selectors, } from "@playwright/test";
5
- export { test } from "./fixtures/index.js";
5
+ import { test } from "./fixtures/index.js";
6
+ export { test };
7
+ export default test;
6
8
  export { expect } from "./fixtures/moduleExpect.js";
7
9
  export { SelaReporter } from "./reporter/SelaReporter.js";
8
10
  export type { SelaReporterOpts, ReporterEngine, ReporterLogger, } from "./reporter/SelaReporter.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGzE,mBAAmB,kBAAkB,CAAC;AAGtC,OAAO,EACL,QAAQ,EACR,OAAO,EACP,MAAM,EACN,OAAO,EACP,SAAS,GACV,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAI3C,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAKpD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,cAAc,GACf,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGzE,mBAAmB,kBAAkB,CAAC;AAGtC,OAAO,EACL,QAAQ,EACR,OAAO,EACP,MAAM,EACN,OAAO,EACP,SAAS,GACV,MAAM,kBAAkB,CAAC;AAU1B,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,CAAC;AAChB,eAAe,IAAI,CAAC;AAIpB,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAKpD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,cAAc,GACf,MAAM,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -17,9 +17,16 @@ Object.defineProperty(exports, "webkit", { enumerable: true, get: function () {
17
17
  Object.defineProperty(exports, "devices", { enumerable: true, get: function () { return test_1.devices; } });
18
18
  Object.defineProperty(exports, "selectors", { enumerable: true, get: function () { return test_1.selectors; } });
19
19
  // Sela's extended test - includes DNA capture, trace collection,
20
- // locator proxy, and fixture-scoped healing expect
21
- var index_js_1 = require("./fixtures/index.js");
20
+ // locator proxy, and fixture-scoped healing expect.
21
+ //
22
+ // Exported as BOTH named and default to mirror Playwright's contract
23
+ // (`export const test; export default test;`). The callable Object.assign
24
+ // root that also makes `require('sela-core')` return `test` lives in
25
+ // `index.cts`; this module is the ESM-facing surface (static named exports
26
+ // the cjs-module-lexer can detect for `import { test, expect }`).
27
+ const index_js_1 = require("./fixtures/index.js");
22
28
  Object.defineProperty(exports, "test", { enumerable: true, get: function () { return index_js_1.test; } });
29
+ exports.default = index_js_1.test;
23
30
  // Sela's healing expect - works standalone (import { expect } from 'sela-core')
24
31
  // Detects page from locator, falls back to raw PW expect for non-locators
25
32
  var moduleExpect_js_1 = require("./fixtures/moduleExpect.js");
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "sela-core",
3
- "version": "0.1.0-alpha.41",
3
+ "version": "0.1.0-alpha.42",
4
4
  "description": "AI self-healing Playwright wrapper - drop-in replacement for @playwright/test",
5
- "main": "./dist/index.js",
5
+ "main": "./dist/index.cjs",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
- "default": "./dist/index.js"
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs",
12
+ "default": "./dist/index.cjs"
11
13
  },
12
14
  "./reporter": {
13
15
  "types": "./dist/reporter/SelaReporter.d.ts",