sela-core 1.0.7 → 1.0.8
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/cli/ErrorHandler.d.ts +1 -1
- package/dist/cli/ErrorHandler.js +14 -14
- package/dist/cli/commands/bulk.d.ts +1 -1
- package/dist/cli/commands/bulk.d.ts.map +1 -1
- package/dist/cli/commands/bulk.js +51 -33
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +26 -18
- package/dist/cli/commands/showReport.d.ts +1 -1
- package/dist/cli/commands/showReport.d.ts.map +1 -1
- package/dist/cli/commands/showReport.js +12 -10
- package/dist/cli/commands/status.d.ts +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +8 -8
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +38 -30
- package/dist/cli/index.js +9 -9
- package/dist/cli/ui/DnaTable.d.ts +1 -1
- package/dist/cli/ui/DnaTable.d.ts.map +1 -1
- package/dist/cli/ui/DnaTable.js +23 -16
- package/dist/cli/ui/RefactorWizard.d.ts +3 -3
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
- package/dist/cli/ui/RefactorWizard.js +108 -82
- package/dist/config/ConfigLoader.d.ts +2 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +15 -15
- package/dist/config/DryRunGuard.d.ts +1 -1
- package/dist/config/DryRunGuard.js +7 -7
- package/dist/config/SelaConfig.d.ts +12 -16
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +32 -26
- package/dist/engine/HealingRegistry.d.ts +1 -1
- package/dist/engine/HealingRegistry.js +3 -3
- package/dist/engine/SelaEngine.d.ts +1 -1
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +63 -49
- package/dist/errors/SelaError.d.ts +0 -1
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +6 -7
- package/dist/fixtures/expectProxy.d.ts +1 -1
- package/dist/fixtures/expectProxy.js +11 -11
- package/dist/fixtures/index.js +14 -14
- package/dist/fixtures/proxyTag.d.ts +1 -1
- package/dist/fixtures/proxyTag.js +3 -3
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/reporter/SelaReporter.d.ts +45 -12
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +139 -39
- package/dist/services/ASTSourceUpdater.d.ts +3 -3
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +92 -68
- package/dist/services/ArgumentTypeAnalyzer.js +7 -7
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
- package/dist/services/BlastRadiusAnalyzer.js +5 -2
- package/dist/services/ChainValidator.js +22 -22
- package/dist/services/CrossFileHealer.js +7 -7
- package/dist/services/DefinitionTracer.d.ts +1 -1
- package/dist/services/DefinitionTracer.d.ts.map +1 -1
- package/dist/services/DefinitionTracer.js +28 -13
- package/dist/services/DnaEditorService.d.ts +2 -2
- package/dist/services/DnaEditorService.d.ts.map +1 -1
- package/dist/services/DnaEditorService.js +81 -62
- package/dist/services/DnaIndexService.d.ts +1 -1
- package/dist/services/DnaIndexService.d.ts.map +1 -1
- package/dist/services/DnaIndexService.js +14 -14
- package/dist/services/HealReportService.d.ts +6 -6
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +33 -29
- package/dist/services/HealingAdvisory.js +1 -1
- package/dist/services/HealingCacheService.d.ts +8 -8
- package/dist/services/HealingCacheService.d.ts.map +1 -1
- package/dist/services/HealingCacheService.js +24 -24
- package/dist/services/HealthReportService.d.ts +1 -1
- package/dist/services/HealthReportService.d.ts.map +1 -1
- package/dist/services/HealthReportService.js +25 -25
- package/dist/services/InitializerUpdater.js +2 -2
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +32 -21
- package/dist/services/InteractiveReview.d.ts +8 -17
- package/dist/services/InteractiveReview.d.ts.map +1 -1
- package/dist/services/InteractiveReview.js +31 -124
- package/dist/services/LLMService.d.ts +1 -1
- package/dist/services/LLMService.js +16 -16
- package/dist/services/PRAutomationService.d.ts +2 -2
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +43 -39
- package/dist/services/PendingPromptLedger.d.ts +2 -2
- package/dist/services/PendingPromptLedger.js +6 -6
- package/dist/services/ReportGenerator.d.ts +2 -2
- package/dist/services/ReportGenerator.d.ts.map +1 -1
- package/dist/services/ReportGenerator.js +5 -5
- package/dist/services/SafetyGuard.d.ts +3 -6
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +29 -106
- package/dist/services/SnapshotService.js +5 -5
- package/dist/services/SourceLinkService.d.ts +2 -2
- package/dist/services/SourceLinkService.d.ts.map +1 -1
- package/dist/services/SourceLinkService.js +31 -22
- package/dist/services/SourceUpdater.d.ts +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +27 -26
- package/dist/services/TemplateDiffService.d.ts.map +1 -1
- package/dist/services/TemplateDiffService.js +18 -15
- package/dist/services/WorkspaceSnapshotService.d.ts +6 -6
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -1
- package/dist/services/WorkspaceSnapshotService.js +10 -12
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/IsolatedDiff.d.ts +2 -2
- package/dist/utils/IsolatedDiff.d.ts.map +1 -1
- package/dist/utils/IsolatedDiff.js +13 -10
- package/package.json +2 -2
|
@@ -91,13 +91,13 @@ class SelaEngine {
|
|
|
91
91
|
// pre-scan. Keeping it separate from the SourceUpdater's project avoids
|
|
92
92
|
// polluting that cache with files we may never actually mutate.
|
|
93
93
|
this.astDirectiveScanner = new ASTSourceUpdater_1.ASTSourceUpdater();
|
|
94
|
-
// Local healing cache
|
|
94
|
+
// Local healing cache - short-circuits the LLM call when an identical
|
|
95
95
|
// (filePath, oldSelector) pair was healed safely in a prior session.
|
|
96
96
|
// Disabled when SELA_NO_CACHE=1; reads/writes skipped during dry runs.
|
|
97
97
|
this.healingCache = HealingCacheService_1.sharedHealingCache;
|
|
98
98
|
}
|
|
99
99
|
// ─────────────────────────────────────────────────────────────
|
|
100
|
-
// healArgument
|
|
100
|
+
// healArgument - repairs a wrong action argument (e.g. selectOption)
|
|
101
101
|
// ─────────────────────────────────────────────────────────────
|
|
102
102
|
async healArgument(page, selector, action, oldArgument, stableId, filePath, line) {
|
|
103
103
|
console.log(`[Sela] 🔧 HEALING ARGUMENT for action '${action}': "${oldArgument}"`);
|
|
@@ -130,7 +130,7 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
132
|
// ─────────────────────────────────────────────────────────────
|
|
133
|
-
// heal
|
|
133
|
+
// heal - repairs a broken selector
|
|
134
134
|
// ─────────────────────────────────────────────────────────────
|
|
135
135
|
async heal(page, fullSelector, elementSelectorOnly, stableId, filePath, line, healMode = "action") {
|
|
136
136
|
console.log(`\n[Sela] 🛠️ HEALING START for: ${stableId}`);
|
|
@@ -148,7 +148,7 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
148
148
|
catch (directiveErr) {
|
|
149
149
|
if ((0, SelaError_1.isSelaErrorFrom)(directiveErr, "ASTUpdater") &&
|
|
150
150
|
directiveErr.code === SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
|
|
151
|
-
console.warn(`[Sela] 🛑 Skipped by Developer
|
|
151
|
+
console.warn(`[Sela] 🛑 Skipped by Developer - ${directiveErr.reason}`);
|
|
152
152
|
const skipEvent = {
|
|
153
153
|
kind: "FAILED",
|
|
154
154
|
stableId,
|
|
@@ -218,10 +218,10 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
218
218
|
return "";
|
|
219
219
|
const lines = [];
|
|
220
220
|
if (anchors.closestLabel) {
|
|
221
|
-
lines.push(`- Label: "${anchors.closestLabel}"
|
|
221
|
+
lines.push(`- Label: "${anchors.closestLabel}" - the UI label or heading associated with this element.`);
|
|
222
222
|
}
|
|
223
223
|
if (anchors.rowAnchor) {
|
|
224
|
-
lines.push(`- Row key: "${anchors.rowAnchor}"
|
|
224
|
+
lines.push(`- Row key: "${anchors.rowAnchor}" - unique cell text identifying the table row this element belonged to.`);
|
|
225
225
|
}
|
|
226
226
|
if (lines.length === 0)
|
|
227
227
|
return "";
|
|
@@ -236,12 +236,12 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
236
236
|
// pair AND SafetyGuard cleared it, replay the cached fix instead
|
|
237
237
|
// of paying the LLM round-trip. Disabled when SELA_NO_CACHE=1.
|
|
238
238
|
// The synthetic aiFix is fed through SafetyGuard exactly like a
|
|
239
|
-
// fresh LLM response
|
|
239
|
+
// fresh LLM response - cache is a latency optimisation, never a
|
|
240
240
|
// safety bypass.
|
|
241
241
|
let aiFix = null;
|
|
242
242
|
let cacheHit = false;
|
|
243
243
|
// Fingerprint inputs the cache key depends on. Recomputed every
|
|
244
|
-
// heal so a DNA rewrite
|
|
244
|
+
// heal so a DNA rewrite - which changes `dnaContentHash` -
|
|
245
245
|
// automatically invalidates every prior entry without an explicit
|
|
246
246
|
// purge.
|
|
247
247
|
const dnaContentHash = (0, HealingCacheService_1.hashDnaContent)(lastKnownState ?? null);
|
|
@@ -255,13 +255,14 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
255
255
|
if (!this.config.noCache) {
|
|
256
256
|
const cached = this.healingCache.get(cacheKeyInput);
|
|
257
257
|
if (cached) {
|
|
258
|
-
console.log(`[Sela] 🧊 Cache hit (0ms LLM)
|
|
258
|
+
console.log(`[Sela] 🧊 Cache hit (0ms LLM) - replaying healed selector "${cached.healedSelector}"`);
|
|
259
259
|
aiFix = {
|
|
260
260
|
status: "FIXED",
|
|
261
261
|
new_selector: cached.healedSelector,
|
|
262
262
|
chainSegments: cached.chainSegments,
|
|
263
|
-
segments: cached.segments ??
|
|
264
|
-
|
|
263
|
+
segments: cached.segments ?? [
|
|
264
|
+
{ type: "element", selector: cached.healedSelector },
|
|
265
|
+
],
|
|
265
266
|
confidence: cached.confidence,
|
|
266
267
|
contentChange: cached.contentChange,
|
|
267
268
|
explanation: cached.explanation ?? "Replayed from local healing cache",
|
|
@@ -308,7 +309,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
308
309
|
console.log(`[Sela] 🔍 Step 3.5: Live text fetch skipped (element not yet rendered or non-text)`);
|
|
309
310
|
}
|
|
310
311
|
// ── Step 3.6: Atomic v2 capture of the candidate element ──
|
|
311
|
-
// Single page.evaluate() call
|
|
312
|
+
// Single page.evaluate() call - collects visibility, ancestry,
|
|
312
313
|
// and anchors of the healed element for SafetyGuard use.
|
|
313
314
|
// Returns null on any failure; never blocks the heal.
|
|
314
315
|
const liveSnapshot = await this.snapshotService.captureElement(page, elementOnlySelector, successfulPath);
|
|
@@ -321,7 +322,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
321
322
|
: "";
|
|
322
323
|
const occFlag = liveVisibility?.isOccluded ? " 🫥 OCCLUDED" : "";
|
|
323
324
|
console.log(`[Sela] 🔍 Step 3.6: Live v2 capture complete` +
|
|
324
|
-
`${ghostFlag}${occFlag}
|
|
325
|
+
`${ghostFlag}${occFlag} - ancestry depth: ${liveAncestry?.length ?? 0}`);
|
|
325
326
|
}
|
|
326
327
|
else {
|
|
327
328
|
console.log(`[Sela] 🔍 Step 3.6: Live v2 capture skipped`);
|
|
@@ -355,7 +356,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
355
356
|
}
|
|
356
357
|
// ── Report: PROTECTED (or BLOCKED non-INCONSISTENT) ─────────
|
|
357
358
|
// The auto-fix was blocked. If the auditor verdict is INCONSISTENT
|
|
358
|
-
// this is a "Security Win"
|
|
359
|
+
// this is a "Security Win" - a masked regression was caught.
|
|
359
360
|
const protectedEvent = {
|
|
360
361
|
kind: "PROTECTED",
|
|
361
362
|
stableId,
|
|
@@ -407,7 +408,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
407
408
|
parts.push(`${breakdown.penalty} ancestry drift`);
|
|
408
409
|
if (breakdown.bonus !== 0)
|
|
409
410
|
parts.push(`+${breakdown.bonus} anchor match`);
|
|
410
|
-
console.log(`[Sela] 📊 Score
|
|
411
|
+
console.log(`[Sela] 📊 Score - AI: ${fmtPctEngine(aiFix.confidence)} | ` +
|
|
411
412
|
`Auditor: ${fmtPctEngine(breakdown.adjustedConfidence)} ` +
|
|
412
413
|
`(raw ${fmtPctEngine(breakdown.rawConfidence)}, ${parts.join(", ")})`);
|
|
413
414
|
}
|
|
@@ -417,7 +418,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
417
418
|
// 1. updateResult.lineUpdated (the strategy's literal write location)
|
|
418
419
|
// 2. updateResult.definitionSite.line (semantic JumpToDef target,
|
|
419
420
|
// used when the mutation crossed into a const/variable declaration)
|
|
420
|
-
// 3. line (the original failure line
|
|
421
|
+
// 3. line (the original failure line - last-resort fallback)
|
|
421
422
|
// For cross-file definitionSite.file, we still read the new line from
|
|
422
423
|
// the SAME file that was mutated.
|
|
423
424
|
const defSite = updateResult.definitionSite;
|
|
@@ -430,7 +431,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
430
431
|
line;
|
|
431
432
|
const mutatedFilePath = !sameFileDef && defSite ? defSite.file : filePath;
|
|
432
433
|
// Clean-Room isolated diff. We diff the in-memory snapshot recorded
|
|
433
|
-
// by WorkspaceSnapshotService (contentBefore
|
|
434
|
+
// by WorkspaceSnapshotService (contentBefore - disk state right
|
|
434
435
|
// before Sela's first write to this file) against the post-write
|
|
435
436
|
// disk state. The result contains EXCLUSIVELY Sela's selector swap;
|
|
436
437
|
// any developer dirty edits sit identically in both buffers and
|
|
@@ -441,12 +442,14 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
441
442
|
// Backward-compat: keep oldCodeLine/newCodeLine populated from the
|
|
442
443
|
// first -/+ pair inside the diff. When git produced no diff (file
|
|
443
444
|
// untracked, git unavailable, …) fall back to a direct disk read of
|
|
444
|
-
// the mutated line
|
|
445
|
-
const oldCodeLine = firstChange.oldLine ??
|
|
446
|
-
|
|
445
|
+
// the mutated line - strictly inferior but preserves old reports.
|
|
446
|
+
const oldCodeLine = firstChange.oldLine ??
|
|
447
|
+
this._safeReadLine(mutatedFilePath, writtenLine);
|
|
448
|
+
const newCodeLine = firstChange.newLine ??
|
|
449
|
+
this._safeReadLine(mutatedFilePath, writtenLine);
|
|
447
450
|
const canonicalSelector = updateResult.healedLocatorString ?? newFullSelector;
|
|
448
451
|
console.log(`[Sela] ✅ Canonical selector (disk == runtime): "${canonicalSelector}"`);
|
|
449
|
-
// Buffer CLI-ready metadata
|
|
452
|
+
// Buffer CLI-ready metadata - merged into DNA at commitUpdates() time.
|
|
450
453
|
// Sanitize filePath: strip any residual "at " stack-trace prefix + whitespace,
|
|
451
454
|
// then resolve to an absolute path before converting to project-relative form.
|
|
452
455
|
const match = filePath.match(/at\s+(.*)/i);
|
|
@@ -515,7 +518,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
515
518
|
contentChange: aiFix.contentChange,
|
|
516
519
|
};
|
|
517
520
|
HealReportService_1.sharedHealReport.record(healedEvent);
|
|
518
|
-
// ── Cache write
|
|
521
|
+
// ── Cache write - persist this safe heal for future sessions ──
|
|
519
522
|
// Best-effort: failures are logged inside the service and never
|
|
520
523
|
// block the heal return. Skipped automatically during dry runs
|
|
521
524
|
// and when SELA_NO_CACHE=1.
|
|
@@ -536,7 +539,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
536
539
|
}
|
|
537
540
|
return canonicalSelector;
|
|
538
541
|
}
|
|
539
|
-
// AI did not return FIXED
|
|
542
|
+
// AI did not return FIXED - record as FAILED.
|
|
540
543
|
const failedEvent = {
|
|
541
544
|
kind: "FAILED",
|
|
542
545
|
stableId,
|
|
@@ -562,9 +565,12 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
562
565
|
// Prefer the structured instanceof check; fall back to message-substring
|
|
563
566
|
// detection for any legacy throw sites that have not yet been migrated.
|
|
564
567
|
const isSafetyBlock = (0, SelaError_1.isSelaErrorFrom)(error, "SafetyGuard") ||
|
|
565
|
-
(typeof error?.message === "string" &&
|
|
566
|
-
|
|
567
|
-
|
|
568
|
+
(typeof error?.message === "string" &&
|
|
569
|
+
error.message.includes("[Sela-SafetyGuard]")) ||
|
|
570
|
+
(typeof error?.message === "string" &&
|
|
571
|
+
error.message.includes("[SafetyGuard]"));
|
|
572
|
+
const isNotFoundAlreadyRecorded = typeof error?.message === "string" &&
|
|
573
|
+
error.message === "AI could not provide a fix";
|
|
568
574
|
if (!isSafetyBlock && !isNotFoundAlreadyRecorded) {
|
|
569
575
|
const failedEvent = {
|
|
570
576
|
kind: "FAILED",
|
|
@@ -594,14 +600,20 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
594
600
|
if (!filePath)
|
|
595
601
|
return "";
|
|
596
602
|
const match = filePath.match(/at\s+(.*)/i);
|
|
597
|
-
const cleaned = (match ? match[1] : filePath)
|
|
598
|
-
|
|
603
|
+
const cleaned = (match ? match[1] : filePath)
|
|
604
|
+
.replace(/^\(|\)$/g, "")
|
|
605
|
+
.trim();
|
|
606
|
+
const abs = path.isAbsolute(cleaned)
|
|
607
|
+
? cleaned
|
|
608
|
+
: path.resolve(process.cwd(), cleaned);
|
|
599
609
|
return path.relative(process.cwd(), abs) || abs;
|
|
600
610
|
}
|
|
601
611
|
_safeReadLine(filePath, lineNumber) {
|
|
602
612
|
try {
|
|
603
613
|
const cleaned = this._toReportFile(filePath);
|
|
604
|
-
const abs = path.isAbsolute(cleaned)
|
|
614
|
+
const abs = path.isAbsolute(cleaned)
|
|
615
|
+
? cleaned
|
|
616
|
+
: path.resolve(process.cwd(), cleaned);
|
|
605
617
|
if (!fs.existsSync(abs))
|
|
606
618
|
return "";
|
|
607
619
|
const lines = fs.readFileSync(abs, "utf8").split(/\r?\n/);
|
|
@@ -654,7 +666,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
654
666
|
* `gitUnifiedDiff` payload yet.
|
|
655
667
|
*
|
|
656
668
|
* Carefully skips the diff preamble (--- a/file, +++ b/file, "diff ",
|
|
657
|
-
* "index ", rename headers)
|
|
669
|
+
* "index ", rename headers) - those lines would otherwise be mistaken for
|
|
658
670
|
* a -/+ change pair and produce nonsense like the original "line replaced
|
|
659
671
|
* by itself" output. Real change lines only count once the parser has seen
|
|
660
672
|
* a "@@" hunk header.
|
|
@@ -694,7 +706,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
694
706
|
if (!meta || !meta.auditorVerdict)
|
|
695
707
|
return null;
|
|
696
708
|
// Keep auditor confidence as `undefined` when SafetyGuard did not produce
|
|
697
|
-
// one
|
|
709
|
+
// one - the renderer fmtPct helper turns it into "n/a" rather than 0%.
|
|
698
710
|
const conf = typeof meta.auditorConfidence === "number" &&
|
|
699
711
|
Number.isFinite(meta.auditorConfidence)
|
|
700
712
|
? meta.auditorConfidence
|
|
@@ -766,15 +778,16 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
766
778
|
_buildReasoningSteps(ctx) {
|
|
767
779
|
const steps = [];
|
|
768
780
|
if (ctx.dnaTag || ctx.dnaText) {
|
|
769
|
-
steps.push(`Recovered DNA baseline
|
|
770
|
-
(ctx.dnaText ? ` with text "${ctx.dnaText}"` : "") +
|
|
781
|
+
steps.push(`Recovered DNA baseline - previously a <${ctx.dnaTag ?? "?"}>` +
|
|
782
|
+
(ctx.dnaText ? ` with text "${ctx.dnaText}"` : "") +
|
|
783
|
+
".");
|
|
771
784
|
}
|
|
772
785
|
if (ctx.successfulPath.length > 0) {
|
|
773
786
|
steps.push(`Resolved DOM context inside ${ctx.successfulPath.length} nested frame(s): ${ctx.successfulPath.join(" >> ")}.`);
|
|
774
787
|
}
|
|
775
788
|
const firstSeg = ctx.aiFix.chainSegments?.[0];
|
|
776
789
|
if (firstSeg) {
|
|
777
|
-
steps.push(`AI preserved the ${firstSeg.type} pattern
|
|
790
|
+
steps.push(`AI preserved the ${firstSeg.type} pattern - semantic locator over raw CSS path.`);
|
|
778
791
|
}
|
|
779
792
|
if (ctx.aiFix.explanation) {
|
|
780
793
|
steps.push(`AI reasoning: ${ctx.aiFix.explanation}`);
|
|
@@ -783,7 +796,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
783
796
|
if (ctx.aiFix.contentChange) {
|
|
784
797
|
steps.push(`Detected content drift: "${ctx.aiFix.contentChange.oldText}" → "${ctx.aiFix.contentChange.newText}".`);
|
|
785
798
|
}
|
|
786
|
-
steps.push(`SafetyGuard verdict: ${ctx.safetyLevel}
|
|
799
|
+
steps.push(`SafetyGuard verdict: ${ctx.safetyLevel} - change cleared for write.`);
|
|
787
800
|
steps.push(`Source updated via ${ctx.strategy} at line ${ctx.writtenLine}.`);
|
|
788
801
|
if (ctx.blastRadius != null && ctx.blastRadius > 0) {
|
|
789
802
|
steps.push(`Blast radius: ${ctx.blastRadius} dependent test reference(s) inherited the fix.`);
|
|
@@ -808,7 +821,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
808
821
|
this.testTitleBuffer.clear();
|
|
809
822
|
SourceUpdater_1.SourceUpdater.flushAdvisories();
|
|
810
823
|
}
|
|
811
|
-
// ── Sela Insights
|
|
824
|
+
// ── Sela Insights - render the transparent healing report ───────
|
|
812
825
|
let reportHtmlPath = null;
|
|
813
826
|
const healedEvents = HealReportService_1.sharedHealReport.getHealedEvents();
|
|
814
827
|
const protectedEvents = HealReportService_1.sharedHealReport.getProtectedEvents();
|
|
@@ -827,11 +840,12 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
827
840
|
// ── Dual-Mode post-run lifecycle ─────────────────────────────────
|
|
828
841
|
//
|
|
829
842
|
// CI (process.env.CI truthy) → Clean-Room PR automation (existing flow).
|
|
830
|
-
// Local DX (CI falsy) →
|
|
831
|
-
//
|
|
832
|
-
//
|
|
843
|
+
// Local DX (CI falsy) → Write-and-notify: AST mutations are
|
|
844
|
+
// already on disk; the reporter prints a
|
|
845
|
+
// diff + vscode:// deeplink + report link
|
|
846
|
+
// via printHealReview() (no Y/N prompt).
|
|
833
847
|
//
|
|
834
|
-
// The
|
|
848
|
+
// The local branch deliberately bypasses PR automation because:
|
|
835
849
|
// • AST fixes are ALREADY persisted to disk (the test passed on them).
|
|
836
850
|
// • Local devs want to inspect + cherry-pick, not auto-commit.
|
|
837
851
|
if (isCiEnvironment()) {
|
|
@@ -858,11 +872,11 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
858
872
|
}
|
|
859
873
|
}
|
|
860
874
|
else if (healedEvents.length > 0) {
|
|
861
|
-
// Local DX:
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
875
|
+
// Local DX: serialise the mutated snapshots to a cross-process
|
|
876
|
+
// ledger. The SelaReporter drains it from `onEnd()` in the main
|
|
877
|
+
// process and prints a non-blocking summary (diff + vscode://
|
|
878
|
+
// deeplink). Mutations are already on disk - the ledger only
|
|
879
|
+
// exists so the reporter knows what to narrate.
|
|
866
880
|
try {
|
|
867
881
|
(0, PendingPromptLedger_1.writePendingPromptsLedger)({
|
|
868
882
|
cwd: process.cwd(),
|
|
@@ -887,12 +901,12 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
887
901
|
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.clear();
|
|
888
902
|
}
|
|
889
903
|
// ─────────────────────────────────────────────────────────────
|
|
890
|
-
// fetchLiveText
|
|
904
|
+
// fetchLiveText - frame-aware innerText fetch (zero-trust probe)
|
|
891
905
|
//
|
|
892
906
|
// Navigates the frame chain encoded in successfulPath, then calls
|
|
893
907
|
// .first().innerText() on the element-only selector. Uses a short
|
|
894
908
|
// 1.5 s timeout so it never adds meaningful latency. Returns null
|
|
895
|
-
// on ANY failure
|
|
909
|
+
// on ANY failure - the caller must treat null as "no data" and skip
|
|
896
910
|
// live verification rather than blocking the heal.
|
|
897
911
|
// ─────────────────────────────────────────────────────────────
|
|
898
912
|
async fetchLiveText(page, elementOnlySelector, framePath) {
|
|
@@ -923,7 +937,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
923
937
|
p === "frame" ||
|
|
924
938
|
p.includes("frame-"));
|
|
925
939
|
if (aiHasFrameSegments) {
|
|
926
|
-
console.log(`[Sela] 🔗 AI returned full frame path
|
|
940
|
+
console.log(`[Sela] 🔗 AI returned full frame path - using directly: ${aiNewSelector}`);
|
|
927
941
|
return aiNewSelector;
|
|
928
942
|
}
|
|
929
943
|
return [...frameParts, ...aiParts].join(" >> ");
|
|
@@ -951,7 +965,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
951
965
|
return arrayOfFiles;
|
|
952
966
|
}
|
|
953
967
|
// ─────────────────────────────────────────────────────────────
|
|
954
|
-
// captureSuccessfulElement
|
|
968
|
+
// captureSuccessfulElement - capture and buffer a v2 DNA snapshot.
|
|
955
969
|
//
|
|
956
970
|
// Replaces the previous DOMUtils.getElementDNA() path with a single
|
|
957
971
|
// captureElement() call that collects all v2 metrics atomically.
|
|
@@ -63,7 +63,6 @@ export declare const SAFETY_GUARD_CODES: {
|
|
|
63
63
|
readonly BLOCKED_ROLE_SWAP_ASSERTION: "SG_BLOCKED_ROLE_SWAP_ASSERTION";
|
|
64
64
|
readonly FAIL_HARD_AUDITOR_INCONSISTENT: "SG_FAIL_HARD_AUDITOR_INCONSISTENT";
|
|
65
65
|
readonly BLOCKED_AUDITOR_SUSPICIOUS: "SG_BLOCKED_AUDITOR_SUSPICIOUS";
|
|
66
|
-
readonly BLOCKED_SIMILARITY_FLOOR: "SG_BLOCKED_SIMILARITY_FLOOR";
|
|
67
66
|
/** Generic dispatcher when SafetyDecision arrives without an explicit code. */
|
|
68
67
|
readonly GENERIC_REJECTION: "SG_GENERIC_REJECTION";
|
|
69
68
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaError.d.ts","sourceRoot":"","sources":["../../src/errors/SelaError.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,aAAa,GACrB,aAAa,GACb,eAAe,GACf,YAAY,GACZ,OAAO,GACP,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,QAAQ,GACR,QAAQ,CAAC;AAMb,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,iBAAiB,GAAG,MAAM,CAAC;IACnE,0EAA0E;IAC1E,cAAc,CAAC,EAAE,YAAY,GAAG,cAAc,GAAG,YAAY,CAAC;IAC9D,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAMD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,aAAa,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,aAAa,CAAC;IACzB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAYD,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAEvB,IAAI,EAAE,aAAa;
|
|
1
|
+
{"version":3,"file":"SelaError.d.ts","sourceRoot":"","sources":["../../src/errors/SelaError.ts"],"names":[],"mappings":"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"}
|
package/dist/errors/SelaError.js
CHANGED
|
@@ -17,7 +17,7 @@ exports.CHAIN_VALIDATOR_CODES = exports.REPORT_CODES = exports.REGISTRY_CODES =
|
|
|
17
17
|
exports.isSelaError = isSelaError;
|
|
18
18
|
exports.isSelaErrorFrom = isSelaErrorFrom;
|
|
19
19
|
// ───────────────────────────────────────────────────────────────────
|
|
20
|
-
// SelaError
|
|
20
|
+
// SelaError - single class for the whole hierarchy.
|
|
21
21
|
//
|
|
22
22
|
// We intentionally avoid a tree of subclasses; `instanceof SelaError` +
|
|
23
23
|
// `err.subsystem === "X"` is more flexible for callers and keeps the
|
|
@@ -31,7 +31,7 @@ class SelaError extends Error {
|
|
|
31
31
|
context;
|
|
32
32
|
constructor(init) {
|
|
33
33
|
if (!CODE_PATTERN.test(init.code)) {
|
|
34
|
-
throw new Error(`[SelaError] invalid code "${init.code}"
|
|
34
|
+
throw new Error(`[SelaError] invalid code "${init.code}" - must match ${CODE_PATTERN}`);
|
|
35
35
|
}
|
|
36
36
|
const formatted = SelaError.formatMessage(init.subsystem, init.code, init.reason);
|
|
37
37
|
super(formatted);
|
|
@@ -45,9 +45,9 @@ class SelaError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
// Maintain prototype chain through transpilation (ES5 target).
|
|
47
47
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
48
|
-
if (typeof Error.captureStackTrace ===
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
if (typeof Error.captureStackTrace ===
|
|
49
|
+
"function") {
|
|
50
|
+
Error.captureStackTrace(this, new.target);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
static formatMessage(subsystem, code, reason) {
|
|
@@ -81,7 +81,7 @@ function isSelaErrorFrom(err, subsystem) {
|
|
|
81
81
|
// ───────────────────────────────────────────────────────────────────
|
|
82
82
|
// Canonical code catalogue
|
|
83
83
|
//
|
|
84
|
-
// Tests assert against these constants
|
|
84
|
+
// Tests assert against these constants - never hand-write code strings
|
|
85
85
|
// at throw sites. Adding a new code requires adding it here first so
|
|
86
86
|
// the catalogue stays in sync with the error-indicativity contract.
|
|
87
87
|
// ───────────────────────────────────────────────────────────────────
|
|
@@ -94,7 +94,6 @@ exports.SAFETY_GUARD_CODES = {
|
|
|
94
94
|
BLOCKED_ROLE_SWAP_ASSERTION: "SG_BLOCKED_ROLE_SWAP_ASSERTION",
|
|
95
95
|
FAIL_HARD_AUDITOR_INCONSISTENT: "SG_FAIL_HARD_AUDITOR_INCONSISTENT",
|
|
96
96
|
BLOCKED_AUDITOR_SUSPICIOUS: "SG_BLOCKED_AUDITOR_SUSPICIOUS",
|
|
97
|
-
BLOCKED_SIMILARITY_FLOOR: "SG_BLOCKED_SIMILARITY_FLOOR",
|
|
98
97
|
/** Generic dispatcher when SafetyDecision arrives without an explicit code. */
|
|
99
98
|
GENERIC_REJECTION: "SG_GENERIC_REJECTION",
|
|
100
99
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Locator } from "@playwright/test";
|
|
2
2
|
import { SelaEngine } from "../engine/SelaEngine";
|
|
3
3
|
/**
|
|
4
|
-
* createHealingExpect
|
|
4
|
+
* createHealingExpect - drop-in replacement for Playwright's expect().
|
|
5
5
|
*
|
|
6
6
|
* @param resolveProxy Optional callback that unwraps a Fixwright proxy
|
|
7
7
|
* to its current live Locator. Supplied by
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/fixtures/expectProxy.ts
|
|
2
|
+
// src/fixtures/expectProxy.ts - full replacement
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.createHealingExpect = createHealingExpect;
|
|
5
5
|
const test_1 = require("@playwright/test");
|
|
@@ -17,7 +17,7 @@ function isElementNotFoundError(err) {
|
|
|
17
17
|
return ELEMENT_NOT_FOUND_PATTERNS.some((p) => p.test(err.message));
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Returns true only for genuine Playwright Locator instances
|
|
20
|
+
* Returns true only for genuine Playwright Locator instances -
|
|
21
21
|
* not FrameLocator proxies or Fixwright proxy objects.
|
|
22
22
|
*
|
|
23
23
|
* A real Locator has _frame internally.
|
|
@@ -31,7 +31,7 @@ function isRealLocator(value) {
|
|
|
31
31
|
if (typeof value !== "object")
|
|
32
32
|
return false;
|
|
33
33
|
// Fixwright locator proxies expose `_fixwrightSelector`. The proxy forwards
|
|
34
|
-
// `_frame` via Reflect.get so `hasFrame` would lie
|
|
34
|
+
// `_frame` via Reflect.get so `hasFrame` would lie - explicitly reject them
|
|
35
35
|
// here. Callers should unwrap via `resolveProxy` first.
|
|
36
36
|
if (value._fixwrightSelector !== undefined)
|
|
37
37
|
return false;
|
|
@@ -63,7 +63,7 @@ function extractSelectorFromLocator(locator) {
|
|
|
63
63
|
* frame segments (e.g. "#modern-frame-container >> #status-label-v2").
|
|
64
64
|
*
|
|
65
65
|
* A plain page.locator() call with a frame >> element string is NOT
|
|
66
|
-
* a valid locator for frame content
|
|
66
|
+
* a valid locator for frame content - Playwright rejects it with
|
|
67
67
|
* "toHaveText can be only used with Locator object".
|
|
68
68
|
*
|
|
69
69
|
* This function correctly chains frameLocator() calls for each frame
|
|
@@ -128,13 +128,13 @@ function wrapMatcher(matcher, locator, matcherName, engine, page, ctx) {
|
|
|
128
128
|
if (!rawSelector)
|
|
129
129
|
throw err;
|
|
130
130
|
console.log(`[Sela] ⚠️ Assertion failed for "${rawSelector}", attempting heal…`);
|
|
131
|
-
// Fast path
|
|
131
|
+
// Fast path - already healed in this test via registry
|
|
132
132
|
const resolved = HealingRegistry_1.HealingRegistry.getInstance().resolveSelector(rawSelector, ctx);
|
|
133
133
|
let healedSelector = resolved;
|
|
134
134
|
if (resolved === rawSelector) {
|
|
135
|
-
// Slow path
|
|
135
|
+
// Slow path - ask the engine + LLM.
|
|
136
136
|
// engine.heal() writes the fix to disk AND returns the canonical
|
|
137
|
-
// healedLocatorString from SourceUpdater
|
|
137
|
+
// healedLocatorString from SourceUpdater - the exact same string
|
|
138
138
|
// that was saved to the file. This is the handshake that guarantees
|
|
139
139
|
// disk and runtime are in sync (no "ghost failures").
|
|
140
140
|
const stableId = require("crypto")
|
|
@@ -182,7 +182,7 @@ const WRAPPED_MATCHERS = [
|
|
|
182
182
|
"toBeFocused",
|
|
183
183
|
];
|
|
184
184
|
/**
|
|
185
|
-
* createHealingExpect
|
|
185
|
+
* createHealingExpect - drop-in replacement for Playwright's expect().
|
|
186
186
|
*
|
|
187
187
|
* @param resolveProxy Optional callback that unwraps a Fixwright proxy
|
|
188
188
|
* to its current live Locator. Supplied by
|
|
@@ -204,7 +204,7 @@ resolveProxy) {
|
|
|
204
204
|
return initialLine;
|
|
205
205
|
};
|
|
206
206
|
const actualLine = getCallerLine(); // כאן אנחנו מקבלים את השורה הנכונה!
|
|
207
|
-
// Step 1
|
|
207
|
+
// Step 1 - unwrap Fixwright proxy to real active Locator
|
|
208
208
|
let resolved = locatorOrValue;
|
|
209
209
|
if (resolveProxy) {
|
|
210
210
|
const unwrapped = resolveProxy(locatorOrValue);
|
|
@@ -217,7 +217,7 @@ resolveProxy) {
|
|
|
217
217
|
if (base === null || typeof base !== "object") {
|
|
218
218
|
return base;
|
|
219
219
|
}
|
|
220
|
-
// Step 2
|
|
220
|
+
// Step 2 - only wrap matchers for genuine page-level Locator objects.
|
|
221
221
|
if (!isRealLocator(resolved)) {
|
|
222
222
|
return base;
|
|
223
223
|
}
|
|
@@ -226,7 +226,7 @@ resolveProxy) {
|
|
|
226
226
|
const ctx = { filePath, line: actualLine };
|
|
227
227
|
const proxy = new Proxy(base, {
|
|
228
228
|
get(target, prop) {
|
|
229
|
-
// Sela proxy identity
|
|
229
|
+
// Sela proxy identity - honored before any string-only branch so
|
|
230
230
|
// callers can detect a Sela expect proxy via Symbol.for("sela.proxy").
|
|
231
231
|
if (prop === proxyTag_1.SELA_PROXY)
|
|
232
232
|
return "expect";
|
package/dist/fixtures/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const fs = __importStar(require("fs"));
|
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const registry = HealingRegistry_1.HealingRegistry.getInstance();
|
|
49
49
|
const proxyToActiveLocator = new WeakMap();
|
|
50
|
-
// Module-level resolver
|
|
50
|
+
// Module-level resolver - moduleExpect.ts uses this to unwrap a Fixwright
|
|
51
51
|
// locator proxy back to its live underlying Playwright Locator without
|
|
52
52
|
// needing access to the WeakMap directly.
|
|
53
53
|
function resolveFixwrightProxy(value) {
|
|
@@ -157,7 +157,7 @@ const GET_BY_METHODS = {
|
|
|
157
157
|
getByTitle: (args) => `internal:attr=[title=${JSON.stringify(args[0])}]`,
|
|
158
158
|
};
|
|
159
159
|
// ─────────────────────────────────────────────────────────────────
|
|
160
|
-
// buildLiveLocator
|
|
160
|
+
// buildLiveLocator - registry-aware
|
|
161
161
|
//
|
|
162
162
|
// NEW: resolves the selector through the HealingRegistry before
|
|
163
163
|
// constructing the Playwright Locator. Zero cost when no heal has
|
|
@@ -240,7 +240,7 @@ function buildHealedArgs(prop, args, timeout = 8000) {
|
|
|
240
240
|
// runWithHealArgument (unchanged from original)
|
|
241
241
|
// ─────────────────────────────────────────────────────────────────
|
|
242
242
|
async function runWithHealArgument(rawPage, prop, args, fullSelector, stableId, callerInfo, originalError) {
|
|
243
|
-
console.log(`[Sela] 💡 Element visible, action '${prop}' failed
|
|
243
|
+
console.log(`[Sela] 💡 Element visible, action '${prop}' failed - healing argument…`);
|
|
244
244
|
const oldArgument = typeof args[0] === "string" ? args[0] : JSON.stringify(args[0]);
|
|
245
245
|
const newArgument = await singleton_1.sharedEngine.healArgument(rawPage, fullSelector, prop, oldArgument, stableId, callerInfo.filePath, callerInfo.line);
|
|
246
246
|
if (!newArgument)
|
|
@@ -250,11 +250,11 @@ async function runWithHealArgument(rawPage, prop, args, fullSelector, stableId,
|
|
|
250
250
|
return await locator[prop](...healedArgs);
|
|
251
251
|
}
|
|
252
252
|
// ─────────────────────────────────────────────────────────────────
|
|
253
|
-
// runWithHeal
|
|
253
|
+
// runWithHeal - pre-flight registry check added
|
|
254
254
|
//
|
|
255
255
|
// NEW: Before the first Playwright attempt, resolve the selector
|
|
256
256
|
// through the registry. If a previous heal already mapped it, the
|
|
257
|
-
// first attempt uses the healed selector and succeeds immediately
|
|
257
|
+
// first attempt uses the healed selector and succeeds immediately -
|
|
258
258
|
// no timeout, no AI call, no file write.
|
|
259
259
|
// ─────────────────────────────────────────────────────────────────
|
|
260
260
|
async function runWithHeal(rawPage, prop, args, fullSelector, elementSelectorOnly, stableId, callerInfo) {
|
|
@@ -451,7 +451,7 @@ function createFrameLocatorProxy(rawFrameLocator, frameSelector, rawPage, testTi
|
|
|
451
451
|
: frameSelector;
|
|
452
452
|
return new Proxy(rawFrameLocator, {
|
|
453
453
|
get(fTarget, fProp) {
|
|
454
|
-
// Sela proxy identity
|
|
454
|
+
// Sela proxy identity - honor before the string-only fast path so
|
|
455
455
|
// symbol probes from tests / debuggers return the kind tag.
|
|
456
456
|
if (fProp === proxyTag_1.SELA_PROXY)
|
|
457
457
|
return "framelocator";
|
|
@@ -468,13 +468,13 @@ function createFrameLocatorProxy(rawFrameLocator, frameSelector, rawPage, testTi
|
|
|
468
468
|
});
|
|
469
469
|
}
|
|
470
470
|
// ─────────────────────────────────────────────────────────────────
|
|
471
|
-
// createLocatorProxy
|
|
471
|
+
// createLocatorProxy - Runtime Selector Injection added
|
|
472
472
|
//
|
|
473
473
|
// NEW additions vs original:
|
|
474
474
|
// 1. Resolves selector through registry on creation (handles proxies
|
|
475
475
|
// born after a heal has already broadcast).
|
|
476
476
|
// 2. Subscribes to registry so any future broadcast swaps
|
|
477
|
-
// activeLocator on the same tick
|
|
477
|
+
// activeLocator on the same tick - before the next await.
|
|
478
478
|
// 3. Exposes _fixwrightUnsubscribe so the fixture can clean up.
|
|
479
479
|
// ─────────────────────────────────────────────────────────────────
|
|
480
480
|
function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCounter, elementSelectorOnly) {
|
|
@@ -494,7 +494,7 @@ function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCoun
|
|
|
494
494
|
? buildLiveLocator(rawPage, initialResolved) // already healed
|
|
495
495
|
: rawLocator; // fresh, use as-is
|
|
496
496
|
// ── Subscribe: swap activeLocator when a heal fires ──────────
|
|
497
|
-
// Inside createLocatorProxy
|
|
497
|
+
// Inside createLocatorProxy - full updated subscribe block
|
|
498
498
|
// בתוך createLocatorProxy - תחליף את הבלוק של ה-subscribe בזה:
|
|
499
499
|
const unsubscribe = registry.subscribe((fp, oldSel, newSel) => {
|
|
500
500
|
// בדיקה אם הסלקטור הנוכחי מכיל את הישן או שהוא הישן בעצמו
|
|
@@ -524,7 +524,7 @@ function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCoun
|
|
|
524
524
|
});
|
|
525
525
|
const proxy = new Proxy(rawLocator, {
|
|
526
526
|
get(target, prop, receiver) {
|
|
527
|
-
// Sela proxy identity
|
|
527
|
+
// Sela proxy identity - must be honored before any string-only branch
|
|
528
528
|
// so callers can reliably detect a Sela proxy via Symbol.for("sela.proxy").
|
|
529
529
|
if (prop === proxyTag_1.SELA_PROXY)
|
|
530
530
|
return "locator";
|
|
@@ -657,9 +657,9 @@ function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCoun
|
|
|
657
657
|
return proxy;
|
|
658
658
|
}
|
|
659
659
|
// ─────────────────────────────────────────────────────────────────
|
|
660
|
-
// test fixture
|
|
660
|
+
// test fixture - teardown added
|
|
661
661
|
// ─────────────────────────────────────────────────────────────────
|
|
662
|
-
// ─── expect
|
|
662
|
+
// ─── expect - healing proxy replacing the plain baseExpect proxy ──────────────
|
|
663
663
|
exports.test = test_1.test.extend({
|
|
664
664
|
page: async ({ page }, use, testInfo) => {
|
|
665
665
|
page.on("console", (msg) => {
|
|
@@ -672,7 +672,7 @@ exports.test = test_1.test.extend({
|
|
|
672
672
|
const unsubscribers = [];
|
|
673
673
|
const pageProxy = new Proxy(page, {
|
|
674
674
|
get(target, prop) {
|
|
675
|
-
// Sela proxy identity
|
|
675
|
+
// Sela proxy identity - honored before any other branch so callers
|
|
676
676
|
// can reliably detect a Sela page proxy via Symbol.for("sela.proxy").
|
|
677
677
|
if (prop === proxyTag_1.SELA_PROXY)
|
|
678
678
|
return "page";
|
|
@@ -714,7 +714,7 @@ exports.test = test_1.test.extend({
|
|
|
714
714
|
},
|
|
715
715
|
expect: async ({ page }, use, testInfo) => {
|
|
716
716
|
const healingExpect = (0, expectProxy_1.createHealingExpect)(singleton_1.sharedEngine, page, testInfo.file, 0,
|
|
717
|
-
// Inject proxy resolver
|
|
717
|
+
// Inject proxy resolver - gives expectProxy.ts access to the live
|
|
718
718
|
// active Locator without exposing the WeakMap directly.
|
|
719
719
|
(value) => {
|
|
720
720
|
if (value && typeof value === "object") {
|
|
@@ -3,7 +3,7 @@ export type SelaProxyKind = "locator" | "framelocator" | "page" | "expect";
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns the proxy kind if `v` is a Sela proxy, otherwise `null`.
|
|
5
5
|
*
|
|
6
|
-
* Performs no instanceof checks against Playwright internals
|
|
6
|
+
* Performs no instanceof checks against Playwright internals - relies
|
|
7
7
|
* solely on the `SELA_PROXY` symbol contract. Always safe to call on
|
|
8
8
|
* arbitrary values (null/undefined/primitive → null).
|
|
9
9
|
*/
|