sela-core 0.1.0-alpha.40 → 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.
- package/dist/boot/bootImpl.d.ts.map +1 -1
- package/dist/boot/bootImpl.js +4 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +32 -12
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +29 -19
- package/dist/fixtures/expectProxy.d.ts +31 -7
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +249 -116
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +3 -2
- package/dist/fixtures/moduleExpect.d.ts +2 -1
- package/dist/fixtures/moduleExpect.d.ts.map +1 -1
- package/dist/fixtures/moduleExpect.js +32 -23
- package/dist/index.cjs +26 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/services/HealReportService.js +3 -2
- package/dist/services/PRAutomationService.js +7 -7
- package/dist/services/VisualSnapshotService.d.ts +18 -12
- package/dist/services/VisualSnapshotService.d.ts.map +1 -1
- package/dist/services/VisualSnapshotService.js +58 -43
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootImpl.d.ts","sourceRoot":"","sources":["../../src/boot/bootImpl.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;IAClD,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;IAC9B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAKD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAMlE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAUzD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAKzF;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"bootImpl.d.ts","sourceRoot":"","sources":["../../src/boot/bootImpl.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;IAClD,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;IAC9B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAKD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAMlE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAUzD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAKzF;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAuC7C"}
|
package/dist/boot/bootImpl.js
CHANGED
|
@@ -39,6 +39,10 @@ function bootImpl(deps) {
|
|
|
39
39
|
const manifestPath = deps.env['SELA_REGRESSION_MANIFEST']
|
|
40
40
|
? deps.env['SELA_REGRESSION_MANIFEST']
|
|
41
41
|
: `${deps.cwd()}/.sela/sela-regression-manifest.json`;
|
|
42
|
+
// Expose the resolved path so worker processes (and the fixture's
|
|
43
|
+
// regressionInjector) can discover the manifest without requiring
|
|
44
|
+
// SELA_REGRESSION_MANIFEST to be set explicitly by the user.
|
|
45
|
+
deps.env['SELA_REGRESSION_MANIFEST'] = manifestPath;
|
|
42
46
|
let manifest;
|
|
43
47
|
try {
|
|
44
48
|
manifest = JSON.parse(deps.readFileSync(manifestPath, 'utf-8'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfigLoader.d.ts","sourceRoot":"","sources":["../../src/config/ConfigLoader.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAiB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"ConfigLoader.d.ts","sourceRoot":"","sources":["../../src/config/ConfigLoader.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAiB,MAAM,cAAc,CAAC;AAqOzE,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA+B;IACtD,OAAO,CAAC,MAAM,CAAC,MAAM,CAAuB;IAK5C,OAAO,CAAC,MAAM,CAAC,aAAa,CAA2B;IAEvD;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,IAAI,cAAc;IAqCpC;;;;OAIG;IACH,MAAM,CAAC,YAAY,IAAI,UAAU;IAKjC,OAAO,CAAC,MAAM,CAAC,aAAa;IAsB5B,yEAAyE;IACzE,MAAM,CAAC,SAAS,IAAI,MAAM,GAAG,IAAI;IAKjC,uDAAuD;IACvD,MAAM,CAAC,KAAK,IAAI,IAAI;CAKrB"}
|
|
@@ -170,23 +170,43 @@ function loadRawConfig(configPath) {
|
|
|
170
170
|
}
|
|
171
171
|
// Every attempt failed. Two cases:
|
|
172
172
|
//
|
|
173
|
-
// 1. The
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
173
|
+
// 1. The config cannot be loaded due to a missing or incompatible TypeScript
|
|
174
|
+
// loader. This covers several scenarios that all stem from the same root
|
|
175
|
+
// cause — the Node.js process has no way to execute .ts (or compiled ESM
|
|
176
|
+
// .js) in the current environment:
|
|
177
|
+
// • ERR_UNKNOWN_FILE_EXTENSION / "Unknown file extension .ts" — plain
|
|
178
|
+
// Node.js sees the .ts extension with no registered loader.
|
|
179
|
+
// • "Cannot use import statement outside a module" — the config (or its
|
|
180
|
+
// compiled .js sibling) uses ESM `import` syntax but Node.js is
|
|
181
|
+
// running in CJS mode and cannot load it with require(). Happens when
|
|
182
|
+
// a compiled sela.config.js uses top-level `import`, or when Node 20
|
|
183
|
+
// --experimental-strip-types strips .ts annotations but the project
|
|
184
|
+
// is CJS.
|
|
185
|
+
// • ERR_REQUIRE_ESM / "require() of ES Module" — Node.js detected the
|
|
186
|
+
// file is an ES module and refused the synchronous require() call.
|
|
187
|
+
// All of these are environment/loader mismatches, not config content bugs.
|
|
188
|
+
// Gracefully fall back to defaults so the CLI command (e.g.
|
|
189
|
+
// `sela shadow-regression`) still runs rather than crashing.
|
|
179
190
|
//
|
|
180
191
|
// 2. Genuine config error (syntax, key validation, etc.) → hard throw so CI
|
|
181
192
|
// surfaces the problem instead of silently running on stale defaults.
|
|
182
193
|
const msg = lastError?.message ?? "";
|
|
183
|
-
const
|
|
184
|
-
|
|
194
|
+
const code = lastError?.code ?? "";
|
|
195
|
+
const isLoaderEnvError = configPath.endsWith(".ts") &&
|
|
196
|
+
(code === "ERR_UNKNOWN_FILE_EXTENSION" ||
|
|
197
|
+
code === "ERR_REQUIRE_ESM" ||
|
|
198
|
+
msg.includes("Cannot find module") ||
|
|
185
199
|
msg.includes("Unknown file extension") ||
|
|
186
|
-
msg.includes("ERR_UNKNOWN_FILE_EXTENSION")
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
200
|
+
msg.includes("ERR_UNKNOWN_FILE_EXTENSION") ||
|
|
201
|
+
msg.includes("Cannot use import statement outside a module") ||
|
|
202
|
+
msg.includes("require() of ES Module") ||
|
|
203
|
+
msg.includes("ERR_REQUIRE_ESM"));
|
|
204
|
+
if (isLoaderEnvError) {
|
|
205
|
+
logger_1.logger.warn(`[Sela] sela.config.ts found but cannot be loaded in this Node.js context ` +
|
|
206
|
+
`(no TypeScript loader active, or the compiled .js output uses ESM syntax ` +
|
|
207
|
+
`incompatible with require()). Running on defaults. ` +
|
|
208
|
+
`To use config values in the CLI, compile sela.config.ts to CJS ` +
|
|
209
|
+
`(e.g. tsc --module commonjs) or set SELA_CONFIG_PATH to the compiled file.`);
|
|
190
210
|
return {};
|
|
191
211
|
}
|
|
192
212
|
throw new Error(`[Sela] Failed to load ${path.basename(configPath)}: ${msg || "unknown error"}. ` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAcxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AA6GhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;IAKzD,OAAO,CAAC,qBAAqB,CAAkC;IAK/D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;;IAyCnD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAqEnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAcxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AA6GhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;IAKzD,OAAO,CAAC,qBAAqB,CAAkC;IAK/D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;;IAyCnD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAqEnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAkmClB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAerB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,2BAA2B;IA4BnC,OAAO,CAAC,yBAAyB;IAgDjC,OAAO,CAAC,kBAAkB;IA8D1B,OAAO,CAAC,oBAAoB;IAyD5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5E,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAsJtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM;IAkErB,OAAO,CAAC,MAAM,CAAC,oBAAoB;CAWpC;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -522,20 +522,30 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
522
522
|
// attached to the HealedEvent below and, on success, stored in the
|
|
523
523
|
// DNA record so subsequent heals can show the previous baseline.
|
|
524
524
|
const _vsKey = (0, VisualSnapshotService_1.snapshotKey)(filePath, stableId);
|
|
525
|
-
const
|
|
525
|
+
const _vsCandDest = (0, VisualSnapshotService_1.candidateSnapshotPath)(process.cwd(), _vsKey);
|
|
526
526
|
let _vsCapture = { elementFound: false, capturedAt: null };
|
|
527
527
|
let _vsLiveRelPath = null;
|
|
528
528
|
let _vsDnaRelPath = null;
|
|
529
529
|
let _vsDnaCapturedAt = null;
|
|
530
530
|
try {
|
|
531
|
-
_vsCapture = await (0, VisualSnapshotService_1.captureElementScreenshot)(page, elementOnlySelector, successfulPath,
|
|
531
|
+
_vsCapture = await (0, VisualSnapshotService_1.captureElementScreenshot)(page, elementOnlySelector, successfulPath, _vsCandDest);
|
|
532
532
|
if (_vsCapture.elementFound) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
533
|
+
const _baselineAbsPath = (0, VisualSnapshotService_1.baselineSnapshotPath)(process.cwd(), _vsKey);
|
|
534
|
+
if (isCiEnvironment()) {
|
|
535
|
+
// CI: keep candidate alongside baseline so the report shows before/after.
|
|
536
|
+
_vsLiveRelPath = path.relative(process.cwd(), _vsCandDest).replace(/\\/g, "/");
|
|
537
|
+
if (fs.existsSync(_baselineAbsPath)) {
|
|
538
|
+
_vsDnaRelPath = path.relative(process.cwd(), _baselineAbsPath).replace(/\\/g, "/");
|
|
539
|
+
_vsDnaCapturedAt = lastKnownState?.visualSnapshot?.capturedAt ?? null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// Local: immediately overwrite baseline → only 1 file ever exists.
|
|
544
|
+
(0, VisualSnapshotService_1.promoteCandidateToBaseline)(process.cwd(), _vsKey);
|
|
545
|
+
_vsLiveRelPath = path.relative(process.cwd(), _baselineAbsPath).replace(/\\/g, "/");
|
|
546
|
+
// Before-image is gone (overwritten); suppress the before slot.
|
|
547
|
+
_vsDnaRelPath = null;
|
|
548
|
+
}
|
|
539
549
|
}
|
|
540
550
|
}
|
|
541
551
|
catch {
|
|
@@ -961,7 +971,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
961
971
|
visualSnapshot: _vsCapture.elementFound && _vsCapture.capturedAt
|
|
962
972
|
? {
|
|
963
973
|
path: path
|
|
964
|
-
.relative(process.cwd(), (0, VisualSnapshotService_1.
|
|
974
|
+
.relative(process.cwd(), (0, VisualSnapshotService_1.baselineSnapshotPath)(process.cwd(), _vsKey))
|
|
965
975
|
.replace(/\\/g, "/"),
|
|
966
976
|
capturedAt: _vsCapture.capturedAt,
|
|
967
977
|
}
|
|
@@ -1527,15 +1537,15 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
1527
1537
|
this.dnaBuffer.set(stableId, enriched);
|
|
1528
1538
|
logger_1.logger.debug(`DNA v2 captured for: ${stableId}`);
|
|
1529
1539
|
// ── Visual baseline capture (guarded) ─────────────────────
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
//
|
|
1533
|
-
if (sourceFile) {
|
|
1540
|
+
// Local only: capture a PNG baseline when the file is missing or the
|
|
1541
|
+
// DOM snapshot changed. CI is read-only — baselines are committed
|
|
1542
|
+
// from local dev and must not be regenerated during CI test runs.
|
|
1543
|
+
if (sourceFile && !isCiEnvironment()) {
|
|
1534
1544
|
try {
|
|
1535
1545
|
const vsKey = (0, VisualSnapshotService_1.snapshotKey)(sourceFile, stableId);
|
|
1536
|
-
const
|
|
1537
|
-
const
|
|
1538
|
-
const pngMissing = !
|
|
1546
|
+
const baseDest = (0, VisualSnapshotService_1.baselineSnapshotPath)(process.cwd(), vsKey);
|
|
1547
|
+
const baseStat = fs.existsSync(baseDest) ? fs.statSync(baseDest) : null;
|
|
1548
|
+
const pngMissing = !baseStat || baseStat.size === 0;
|
|
1539
1549
|
let needsCapture = pngMissing;
|
|
1540
1550
|
if (!pngMissing) {
|
|
1541
1551
|
// load() reads from disk — commitUpdates() hasn't run yet,
|
|
@@ -1547,10 +1557,10 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
1547
1557
|
SelaEngine._snapshotFingerprint(enriched);
|
|
1548
1558
|
}
|
|
1549
1559
|
if (needsCapture) {
|
|
1550
|
-
const
|
|
1551
|
-
const result = await (0, VisualSnapshotService_1.captureElementScreenshot)(page, selector, [],
|
|
1560
|
+
const candDest = (0, VisualSnapshotService_1.candidateSnapshotPath)(process.cwd(), vsKey);
|
|
1561
|
+
const result = await (0, VisualSnapshotService_1.captureElementScreenshot)(page, selector, [], candDest);
|
|
1552
1562
|
if (result.elementFound) {
|
|
1553
|
-
(0, VisualSnapshotService_1.
|
|
1563
|
+
(0, VisualSnapshotService_1.promoteCandidateToBaseline)(process.cwd(), vsKey);
|
|
1554
1564
|
logger_1.logger.debug(`Visual baseline ${pngMissing ? "created" : "refreshed"}: ${vsKey}`);
|
|
1555
1565
|
}
|
|
1556
1566
|
}
|
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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,
|
|
11
|
-
|
|
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":"
|
|
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
|
-
*
|
|
118
|
-
*
|
|
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
|
|
121
|
-
return
|
|
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
|
-
|
|
129
|
-
|
|
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(`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAK/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAK/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAuCpD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAI/D;AAgoBD,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,GAAG,EACpB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,IAAI,EACb,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAChC,WAAW,EAAE,MAAM,GAClB,GAAG,CAuCL;AAaD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,OAAO,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,IAAI,EACb,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAChC,mBAAmB,CAAC,EAAE,MAAM,GAC3B,GAAG,CAiPL;AAQD,eAAO,MAAM,IAAI;YAEL,UAAU,CAAC,OAAO,mBAAmB,CAAC;;IAG9C;;;;;;;;;;;;;OAaG;0BACmB,IAAI;EAmI5B,CAAC;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/fixtures/index.js
CHANGED
|
@@ -58,11 +58,12 @@ function regressionInjector() {
|
|
|
58
58
|
if (_regressionInjector !== undefined)
|
|
59
59
|
return _regressionInjector;
|
|
60
60
|
const active = process.env["SELA_REGRESSION_ACTIVE"];
|
|
61
|
-
|
|
62
|
-
if (active?.trim().toLowerCase() !== "true" || !manifestPath) {
|
|
61
|
+
if (active?.trim().toLowerCase() !== "true") {
|
|
63
62
|
_regressionInjector = null;
|
|
64
63
|
return null;
|
|
65
64
|
}
|
|
65
|
+
const manifestPath = process.env["SELA_REGRESSION_MANIFEST"] ??
|
|
66
|
+
path.join(process.cwd(), ".sela", RegressionManifestService_1.REGRESSION_MANIFEST_FILE);
|
|
66
67
|
try {
|
|
67
68
|
_regressionInjector = RegressionManifestService_1.RegressionInjector.fromFile(manifestPath);
|
|
68
69
|
logger_1.logger.debug(`Shadow-regression injector loaded from ${manifestPath}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moduleExpect.d.ts","sourceRoot":"","sources":["../../src/fixtures/moduleExpect.ts"],"names":[],"mappings":"
|
|
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
|
-
//
|
|
4
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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
|
-
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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");
|
|
@@ -1266,6 +1266,7 @@ const REPORT_JS = [
|
|
|
1266
1266
|
"}",
|
|
1267
1267
|
// ── Shared-locator runtime bypass (blast-radius protection)
|
|
1268
1268
|
"var WARN_TRIANGLE='<svg class=\"warn-tri\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 1.5L15 14H1L8 1.5z\"/><line x1=\"8\" y1=\"6\" x2=\"8\" y2=\"9.5\"/><circle cx=\"8\" cy=\"11.6\" r=\"0.7\" fill=\"currentColor\" stroke=\"none\"/></svg>';",
|
|
1269
|
+
"var LIGHTBULB='<svg class=\"lightbulb-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 1a3.5 3.5 0 0 0-3.5 3.5c0 1.5.5 2.5 1 3.5v1.5h5v-1.5c.5-1 1-2 1-3.5A3.5 3.5 0 0 0 8 1z\"/><line x1=\"5\" y1=\"10.5\" x2=\"11\" y2=\"10.5\"/><path d=\"M6 12.5h4\"/><path d=\"M6.5 14h3\"/></svg>';",
|
|
1269
1270
|
"function renderBypassBody(ev){",
|
|
1270
1271
|
" var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
|
|
1271
1272
|
" var files=(ev.affectedFiles&&ev.affectedFiles.length)?'<div class=\"bb-files\">Affected: '+ev.affectedFiles.map(esc).join(', ')+'</div>':'';",
|
|
@@ -1285,7 +1286,7 @@ const REPORT_JS = [
|
|
|
1285
1286
|
" var bypassBrainInner=''",
|
|
1286
1287
|
" +'<div><div class=\"brain-section-title\">Chain of Thought</div>'+renderCoT(steps)+'</div>'",
|
|
1287
1288
|
" +'<div><div class=\"brain-section-title\">Intent Audit</div>'+renderAuditor(ev.auditor||null)+'</div>';",
|
|
1288
|
-
" var bypassBrain='<details class=\"brain-details\"><summary class=\"brain-summary\"><span class=\"brain-arrow\">▶</span
|
|
1289
|
+
" var bypassBrain='<details class=\"brain-details\"><summary class=\"brain-summary\"><span class=\"brain-arrow\">▶</span>'+LIGHTBULB+' AI Reasoning \xb7 Audit</summary><div class=\"brain-body\">'+bypassBrainInner+'</div></details>';",
|
|
1289
1290
|
" var actions='<div class=\"actions\">'",
|
|
1290
1291
|
" +'<a class=\"btn btn-primary\" href=\"'+link+'\">→ Open shared declaration in VS Code</a>'",
|
|
1291
1292
|
" +'</div>';",
|
|
@@ -1344,7 +1345,7 @@ const REPORT_JS = [
|
|
|
1344
1345
|
" var _snip=(_d.tagName||'element')+(_d.role?' role=\"'+_d.role+'\"':'')+(_d.text?' \\''+String(_d.text).slice(0,60)+'\\'':'');",
|
|
1345
1346
|
" brainInner+='<div><div class=\"brain-section-title\">Target DOM Snapshot</div><div class=\"muted-block\" style=\"font-family:var(--mono);font-size:12px\">'+esc(_snip)+'</div></div>';",
|
|
1346
1347
|
" }",
|
|
1347
|
-
" var brain='<details class=\"brain-details\"><summary class=\"brain-summary\"><span class=\"brain-arrow\">▶</span
|
|
1348
|
+
" var brain='<details class=\"brain-details\"><summary class=\"brain-summary\"><span class=\"brain-arrow\">▶</span>'+LIGHTBULB+' AI Reasoning & Audit</summary><div class=\"brain-body\">'+brainInner+'</div></details>';",
|
|
1348
1349
|
' return\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Source Code Diff</div>\'+renderDiff(ev)+\'</div>\'',
|
|
1349
1350
|
" +brain",
|
|
1350
1351
|
" +renderFixRegression(ev)",
|
|
@@ -310,7 +310,7 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
|
|
|
310
310
|
}
|
|
311
311
|
lines.push("```");
|
|
312
312
|
if (h.aiExplanation) {
|
|
313
|
-
lines.push(`>
|
|
313
|
+
lines.push(`> 💡 ${h.aiExplanation}`);
|
|
314
314
|
}
|
|
315
315
|
lines.push(`> AI Confidence: **${fmtPct(h.aiConfidence)}** · Auditor: **${fmtPct(h.auditor?.confidence)}**`);
|
|
316
316
|
lines.push("");
|
|
@@ -483,12 +483,12 @@ async function runCleanRoom(args) {
|
|
|
483
483
|
continue;
|
|
484
484
|
const key = (0, VisualSnapshotService_1.snapshotKey)(heal.sourceFile, heal.stableId);
|
|
485
485
|
_vsActiveKeys.add(key);
|
|
486
|
-
const promoted = (0, VisualSnapshotService_1.
|
|
486
|
+
const promoted = (0, VisualSnapshotService_1.promoteCandidateToBaseline)(ctx.cwd, key);
|
|
487
487
|
if (promoted.ok) {
|
|
488
|
-
const
|
|
489
|
-
.join(".sela", "dna-snapshots", `${key}-
|
|
488
|
+
const relBase = path
|
|
489
|
+
.join(".sela", "dna-snapshots", `${key}-baseline.png`)
|
|
490
490
|
.replace(/\\/g, "/");
|
|
491
|
-
(0, shell_1.sh)(`git add ${(0, shell_1.shellQuote)(
|
|
491
|
+
(0, shell_1.sh)(`git add ${(0, shell_1.shellQuote)(relBase)}`, ctx.cwd);
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
494
|
const gcCount = (0, VisualSnapshotService_1.gcOrphanSnapshots)(ctx.cwd, _vsActiveKeys);
|
|
@@ -496,14 +496,14 @@ async function runCleanRoom(args) {
|
|
|
496
496
|
(0, shell_1.sh)(`git add -u ${(0, shell_1.shellQuote)(path.join(".sela", "dna-snapshots"))}`, ctx.cwd);
|
|
497
497
|
}
|
|
498
498
|
// Stage first-run pass baselines and freshened post-edit baselines.
|
|
499
|
-
// These are *-
|
|
499
|
+
// These are *-baseline.png files written by captureSuccessfulElement()
|
|
500
500
|
// that are not part of the heal loop above (no heal event = no loop
|
|
501
501
|
// entry). git add on already-staged files is a no-op, so double-
|
|
502
502
|
// staging healed PNGs here is harmless.
|
|
503
503
|
const dnaDir = path.join(ctx.cwd, ".sela", "dna-snapshots");
|
|
504
504
|
if (fs.existsSync(dnaDir)) {
|
|
505
505
|
for (const f of fs.readdirSync(dnaDir)) {
|
|
506
|
-
if (!f.endsWith("-
|
|
506
|
+
if (!f.endsWith("-baseline.png"))
|
|
507
507
|
continue;
|
|
508
508
|
(0, shell_1.sh)(`git add ${(0, shell_1.shellQuote)(path.join(".sela", "dna-snapshots", f))}`, ctx.cwd);
|
|
509
509
|
}
|
|
@@ -2,10 +2,15 @@ import type { Page } from "@playwright/test";
|
|
|
2
2
|
/** Deterministic 16-hex key — same truncation as HealingRegistry.fingerprint(). */
|
|
3
3
|
export declare function snapshotKey(sourceFile: string, stableId: string): string;
|
|
4
4
|
export declare function dnaSnapshotDir(cwd: string): string;
|
|
5
|
-
/** Canonical on-disk baseline
|
|
6
|
-
export declare function
|
|
7
|
-
/**
|
|
8
|
-
|
|
5
|
+
/** Canonical on-disk baseline — committed to git; one file per element, ever. */
|
|
6
|
+
export declare function baselineSnapshotPath(cwd: string, key: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Candidate screenshot captured during a heal cycle.
|
|
9
|
+
* - Local: promoted immediately to baseline then deleted (0 surplus files).
|
|
10
|
+
* - CI: kept alongside the untouched baseline so the report shows before/after.
|
|
11
|
+
* PRAutomationService promotes and deletes it when the heal PR is committed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function candidateSnapshotPath(cwd: string, key: string): string;
|
|
9
14
|
export interface CaptureResult {
|
|
10
15
|
elementFound: boolean;
|
|
11
16
|
capturedAt: string | null;
|
|
@@ -19,28 +24,29 @@ export interface CaptureResult {
|
|
|
19
24
|
*/
|
|
20
25
|
export declare function captureElementScreenshot(page: Page, selector: string, framePath: string[], destPath: string): Promise<CaptureResult>;
|
|
21
26
|
/**
|
|
22
|
-
* Atomic promotion of the
|
|
27
|
+
* Atomic promotion of the candidate capture to the canonical baseline path.
|
|
23
28
|
*
|
|
24
29
|
* Steps:
|
|
25
|
-
* 1. Backup existing
|
|
26
|
-
* 2. Copy
|
|
30
|
+
* 1. Backup existing baseline to baseline.bak (so rollback can restore it).
|
|
31
|
+
* 2. Copy candidate → baseline.
|
|
32
|
+
* 3. Delete candidate — baseline is now the single source of truth.
|
|
27
33
|
*
|
|
28
|
-
* Returns {ok:false} when the
|
|
34
|
+
* Returns {ok:false} when the candidate file is absent (capture failed or
|
|
29
35
|
* elementFound was false).
|
|
30
36
|
*/
|
|
31
|
-
export declare function
|
|
37
|
+
export declare function promoteCandidateToBaseline(cwd: string, key: string): {
|
|
32
38
|
ok: boolean;
|
|
33
39
|
};
|
|
34
40
|
/**
|
|
35
41
|
* Undo a promotion by restoring the .bak file (if present) or deleting the
|
|
36
|
-
* newly promoted
|
|
42
|
+
* newly promoted baseline file. Called from runCleanRoom()'s catch block.
|
|
37
43
|
*/
|
|
38
44
|
export declare function rollbackPromotion(cwd: string, key: string): void;
|
|
39
45
|
/** Clean up the .bak sidecar after a successful git commit. */
|
|
40
46
|
export declare function cleanupBackup(cwd: string, key: string): void;
|
|
41
47
|
/**
|
|
42
|
-
* Delete *-
|
|
43
|
-
*
|
|
48
|
+
* Delete *-baseline.png and *-candidate.png files not referenced by any key
|
|
49
|
+
* in activeKeys. Leaves .bak files untouched.
|
|
44
50
|
*
|
|
45
51
|
* Returns the number of files deleted (0 when the dir doesn't exist yet).
|
|
46
52
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VisualSnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/VisualSnapshotService.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAS7C,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKxE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,
|
|
1
|
+
{"version":3,"file":"VisualSnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/VisualSnapshotService.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAS7C,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKxE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtE;AAwCD,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EAAE,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAoCxB;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,CAkCjB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAahE;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAO5D;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GACtB,MAAM,CA4BR"}
|
|
@@ -52,10 +52,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
52
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
53
|
exports.snapshotKey = snapshotKey;
|
|
54
54
|
exports.dnaSnapshotDir = dnaSnapshotDir;
|
|
55
|
-
exports.
|
|
56
|
-
exports.
|
|
55
|
+
exports.baselineSnapshotPath = baselineSnapshotPath;
|
|
56
|
+
exports.candidateSnapshotPath = candidateSnapshotPath;
|
|
57
57
|
exports.captureElementScreenshot = captureElementScreenshot;
|
|
58
|
-
exports.
|
|
58
|
+
exports.promoteCandidateToBaseline = promoteCandidateToBaseline;
|
|
59
59
|
exports.rollbackPromotion = rollbackPromotion;
|
|
60
60
|
exports.cleanupBackup = cleanupBackup;
|
|
61
61
|
exports.gcOrphanSnapshots = gcOrphanSnapshots;
|
|
@@ -77,13 +77,18 @@ function snapshotKey(sourceFile, stableId) {
|
|
|
77
77
|
function dnaSnapshotDir(cwd) {
|
|
78
78
|
return path.join(cwd, SNAPSHOT_SUBDIR);
|
|
79
79
|
}
|
|
80
|
-
/** Canonical on-disk baseline
|
|
81
|
-
function
|
|
82
|
-
return path.join(dnaSnapshotDir(cwd), `${key}-
|
|
80
|
+
/** Canonical on-disk baseline — committed to git; one file per element, ever. */
|
|
81
|
+
function baselineSnapshotPath(cwd, key) {
|
|
82
|
+
return path.join(dnaSnapshotDir(cwd), `${key}-baseline.png`);
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Candidate screenshot captured during a heal cycle.
|
|
86
|
+
* - Local: promoted immediately to baseline then deleted (0 surplus files).
|
|
87
|
+
* - CI: kept alongside the untouched baseline so the report shows before/after.
|
|
88
|
+
* PRAutomationService promotes and deletes it when the heal PR is committed.
|
|
89
|
+
*/
|
|
90
|
+
function candidateSnapshotPath(cwd, key) {
|
|
91
|
+
return path.join(dnaSnapshotDir(cwd), `${key}-candidate.png`);
|
|
87
92
|
}
|
|
88
93
|
function ensureSnapshotDir(cwd) {
|
|
89
94
|
const dir = dnaSnapshotDir(cwd);
|
|
@@ -159,56 +164,62 @@ async function captureElementScreenshot(page, selector, framePath, destPath) {
|
|
|
159
164
|
// PROMOTION (live → dna baseline)
|
|
160
165
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
166
|
/**
|
|
162
|
-
* Atomic promotion of the
|
|
167
|
+
* Atomic promotion of the candidate capture to the canonical baseline path.
|
|
163
168
|
*
|
|
164
169
|
* Steps:
|
|
165
|
-
* 1. Backup existing
|
|
166
|
-
* 2. Copy
|
|
170
|
+
* 1. Backup existing baseline to baseline.bak (so rollback can restore it).
|
|
171
|
+
* 2. Copy candidate → baseline.
|
|
172
|
+
* 3. Delete candidate — baseline is now the single source of truth.
|
|
167
173
|
*
|
|
168
|
-
* Returns {ok:false} when the
|
|
174
|
+
* Returns {ok:false} when the candidate file is absent (capture failed or
|
|
169
175
|
* elementFound was false).
|
|
170
176
|
*/
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const bakPath = `${
|
|
177
|
+
function promoteCandidateToBaseline(cwd, key) {
|
|
178
|
+
const candPath = candidateSnapshotPath(cwd, key);
|
|
179
|
+
const basePath = baselineSnapshotPath(cwd, key);
|
|
180
|
+
const bakPath = `${basePath}.bak`;
|
|
175
181
|
try {
|
|
176
|
-
if (!fs.existsSync(
|
|
177
|
-
logger_1.logger.debug(`VisualSnapshot: promote skipped —
|
|
182
|
+
if (!fs.existsSync(candPath)) {
|
|
183
|
+
logger_1.logger.debug(`VisualSnapshot: promote skipped — candidate not found: ${path.basename(candPath)}`);
|
|
178
184
|
return { ok: false };
|
|
179
185
|
}
|
|
180
186
|
ensureSnapshotDir(cwd);
|
|
181
187
|
// Backup existing baseline for rollback.
|
|
182
|
-
if (fs.existsSync(
|
|
188
|
+
if (fs.existsSync(basePath)) {
|
|
183
189
|
try {
|
|
184
|
-
fs.copyFileSync(
|
|
190
|
+
fs.copyFileSync(basePath, bakPath);
|
|
185
191
|
}
|
|
186
192
|
catch {
|
|
187
193
|
// Backup failure is non-fatal; rollback may not be possible.
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
|
-
fs.copyFileSync(
|
|
196
|
+
fs.copyFileSync(candPath, basePath);
|
|
197
|
+
// Remove candidate — baseline is now the single source of truth.
|
|
198
|
+
try {
|
|
199
|
+
fs.unlinkSync(candPath);
|
|
200
|
+
}
|
|
201
|
+
catch { /* non-fatal */ }
|
|
191
202
|
return { ok: true };
|
|
192
203
|
}
|
|
193
204
|
catch (err) {
|
|
194
|
-
logger_1.logger.warn(`VisualSnapshot:
|
|
205
|
+
logger_1.logger.warn(`VisualSnapshot: promoteCandidateToBaseline failed: ${err?.message ?? err}`);
|
|
195
206
|
return { ok: false };
|
|
196
207
|
}
|
|
197
208
|
}
|
|
198
209
|
/**
|
|
199
210
|
* Undo a promotion by restoring the .bak file (if present) or deleting the
|
|
200
|
-
* newly promoted
|
|
211
|
+
* newly promoted baseline file. Called from runCleanRoom()'s catch block.
|
|
201
212
|
*/
|
|
202
213
|
function rollbackPromotion(cwd, key) {
|
|
203
|
-
const
|
|
204
|
-
const bakPath = `${
|
|
214
|
+
const basePath = baselineSnapshotPath(cwd, key);
|
|
215
|
+
const bakPath = `${basePath}.bak`;
|
|
205
216
|
try {
|
|
206
217
|
if (fs.existsSync(bakPath)) {
|
|
207
|
-
fs.copyFileSync(bakPath,
|
|
218
|
+
fs.copyFileSync(bakPath, basePath);
|
|
208
219
|
fs.unlinkSync(bakPath);
|
|
209
220
|
}
|
|
210
|
-
else if (fs.existsSync(
|
|
211
|
-
fs.unlinkSync(
|
|
221
|
+
else if (fs.existsSync(basePath)) {
|
|
222
|
+
fs.unlinkSync(basePath);
|
|
212
223
|
}
|
|
213
224
|
}
|
|
214
225
|
catch {
|
|
@@ -217,7 +228,7 @@ function rollbackPromotion(cwd, key) {
|
|
|
217
228
|
}
|
|
218
229
|
/** Clean up the .bak sidecar after a successful git commit. */
|
|
219
230
|
function cleanupBackup(cwd, key) {
|
|
220
|
-
const bakPath = `${
|
|
231
|
+
const bakPath = `${baselineSnapshotPath(cwd, key)}.bak`;
|
|
221
232
|
try {
|
|
222
233
|
if (fs.existsSync(bakPath))
|
|
223
234
|
fs.unlinkSync(bakPath);
|
|
@@ -230,8 +241,8 @@ function cleanupBackup(cwd, key) {
|
|
|
230
241
|
// GC (orphan dna file cleanup)
|
|
231
242
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
232
243
|
/**
|
|
233
|
-
* Delete *-
|
|
234
|
-
*
|
|
244
|
+
* Delete *-baseline.png and *-candidate.png files not referenced by any key
|
|
245
|
+
* in activeKeys. Leaves .bak files untouched.
|
|
235
246
|
*
|
|
236
247
|
* Returns the number of files deleted (0 when the dir doesn't exist yet).
|
|
237
248
|
*/
|
|
@@ -242,18 +253,22 @@ function gcOrphanSnapshots(cwd, activeKeys) {
|
|
|
242
253
|
let deleted = 0;
|
|
243
254
|
try {
|
|
244
255
|
for (const file of fs.readdirSync(dir)) {
|
|
245
|
-
|
|
256
|
+
let key;
|
|
257
|
+
if (file.endsWith("-baseline.png")) {
|
|
258
|
+
key = file.slice(0, -"-baseline.png".length);
|
|
259
|
+
}
|
|
260
|
+
else if (file.endsWith("-candidate.png")) {
|
|
261
|
+
key = file.slice(0, -"-candidate.png".length);
|
|
262
|
+
}
|
|
263
|
+
if (!key || activeKeys.has(key))
|
|
246
264
|
continue;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
catch {
|
|
255
|
-
// Best effort.
|
|
256
|
-
}
|
|
265
|
+
try {
|
|
266
|
+
fs.unlinkSync(path.join(dir, file));
|
|
267
|
+
deleted++;
|
|
268
|
+
logger_1.logger.debug(`VisualSnapshot: GC deleted orphan ${file}`);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Best effort.
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface ElementSnapshot {
|
|
|
50
50
|
isInShadowDom?: boolean;
|
|
51
51
|
/** Visual element snapshot captured during a heal event. Path is cwd-relative. */
|
|
52
52
|
visualSnapshot?: {
|
|
53
|
-
/** cwd-relative path to the committed
|
|
53
|
+
/** cwd-relative path to the committed baseline PNG (e.g. .sela/dna-snapshots/<key>-baseline.png). */
|
|
54
54
|
path: string;
|
|
55
55
|
/** ISO timestamp of when the live element screenshot was captured. */
|
|
56
56
|
capturedAt: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,sBAAsB,GACtB,iBAAiB,GACjB,wBAAwB,GACxB,cAAc,GACd,8BAA8B,CAAC;AAEnC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,cAAc,GAAG,IAAI,CAAC;IACnC,UAAU,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,mGAAmG;IACnG,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yFAAyF;IACzF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gGAAgG;IAChG,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAID;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QACL,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF,aAAa,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACtB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,cAAc,CAAC,EAAE;QACf,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,sBAAsB,GACtB,iBAAiB,GACjB,wBAAwB,GACxB,cAAc,GACd,8BAA8B,CAAC;AAEnC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,cAAc,GAAG,IAAI,CAAC;IACnC,UAAU,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,mGAAmG;IACnG,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yFAAyF;IACzF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gGAAgG;IAChG,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAID;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QACL,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF,aAAa,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACtB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,cAAc,CAAC,EAAE;QACf,qGAAqG;QACrG,IAAI,EAAE,MAAM,CAAC;QACb,sEAAsE;QACtE,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG;IAChD,aAAa,EAAE,CAAC,CAAC;IACjB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,EAAE,UAAU,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,iBAAiB,CAEvE;AAID,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE;QACN,eAAe,EAAE,MAAM,CAAC;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,IAAI,EAAE;QACJ,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,SAAS,EAAE;QACT,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC;CACH"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sela-core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
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
|
-
"
|
|
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",
|