sela-core 1.0.0
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/README.md +93 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +140 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +133 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- package/dist/cli/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +123 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +70 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +269 -0
- package/dist/config/ConfigLoader.d.ts +15 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +174 -0
- package/dist/config/SelaConfig.d.ts +67 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +57 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +98 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +228 -0
- package/dist/fixtures/index.d.ts +6 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +688 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +46 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/services/ASTSourceUpdater.d.ts +79 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3177 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +103 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +569 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +134 -0
- package/dist/services/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +350 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +198 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealingAdvisory.d.ts +22 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +42 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +37 -0
- package/dist/services/IntentAuditor.d.ts +39 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +302 -0
- package/dist/services/LLMService.d.ts +100 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +439 -0
- package/dist/services/SafetyGuard.d.ts +65 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +524 -0
- package/dist/services/SnapshotService.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +349 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +156 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1021 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +331 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +31 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +33 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +179 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SourceUpdater = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const ASTSourceUpdater_1 = require("./ASTSourceUpdater");
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────
|
|
42
|
+
// QUOTE MANAGEMENT
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────
|
|
44
|
+
const astUpdater = new ASTSourceUpdater_1.ASTSourceUpdater();
|
|
45
|
+
function detectOuterQuote(line, target) {
|
|
46
|
+
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
for (const q of [`"`, `'`, "`"]) {
|
|
48
|
+
if (new RegExp(`${q}${escaped}${q}`).test(line))
|
|
49
|
+
return q;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function sanitizeForInjection(selector, outerQuote) {
|
|
54
|
+
switch (outerQuote) {
|
|
55
|
+
case `"`:
|
|
56
|
+
return selector.replace(/"/g, `'`);
|
|
57
|
+
case `'`:
|
|
58
|
+
return selector.replace(/'/g, `"`);
|
|
59
|
+
case "`":
|
|
60
|
+
return selector.replace(/`/g, "\\`");
|
|
61
|
+
default:
|
|
62
|
+
return selector.replace(/"/g, `'`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function replaceStringLiteralInLine(line, target, replacement) {
|
|
66
|
+
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67
|
+
for (const q of [`"`, `'`, "`"]) {
|
|
68
|
+
const regex = new RegExp(`${q}${escaped}${q}`);
|
|
69
|
+
if (regex.test(line)) {
|
|
70
|
+
const safeReplacement = sanitizeForInjection(replacement, q);
|
|
71
|
+
return line.replace(regex, `${q}${safeReplacement}${q}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────
|
|
77
|
+
// DEDUPLICATION
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────
|
|
79
|
+
function deduplicateSelector(aiSelector, surroundingChain) {
|
|
80
|
+
const aiParts = aiSelector.split(">>").map((s) => s.trim());
|
|
81
|
+
const chainLower = surroundingChain.toLowerCase();
|
|
82
|
+
let startIdx = 0;
|
|
83
|
+
for (let i = 0; i < aiParts.length - 1; i++) {
|
|
84
|
+
const part = aiParts[i].replace(/['"]/g, "").toLowerCase();
|
|
85
|
+
if (chainLower.includes(part)) {
|
|
86
|
+
startIdx = i + 1;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return aiParts.slice(startIdx).join(" >> ").trim();
|
|
93
|
+
}
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────
|
|
95
|
+
// RUNTIME SELECTOR DEDUPLICATION (FIX 2B)
|
|
96
|
+
//
|
|
97
|
+
// Removes duplicate consecutive `>>` segments from a runtime selector.
|
|
98
|
+
// Uses normalised comparison so minor whitespace / quote differences
|
|
99
|
+
// do not prevent deduplication.
|
|
100
|
+
//
|
|
101
|
+
// Example:
|
|
102
|
+
// "#modern-frame-container >> #modern-frame-container >> internal:control=enter-frame >> ..."
|
|
103
|
+
// → "#modern-frame-container >> internal:control=enter-frame >> ..."
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────
|
|
105
|
+
function deduplicateRuntimeSelector(selector) {
|
|
106
|
+
const parts = selector.split(">>").map((s) => s.trim());
|
|
107
|
+
const deduped = [];
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
const norm = part
|
|
110
|
+
.replace(/['"]/g, "")
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/\s+/g, " ")
|
|
113
|
+
.trim();
|
|
114
|
+
const prevNorm = deduped.length > 0
|
|
115
|
+
? deduped[deduped.length - 1]
|
|
116
|
+
.replace(/['"]/g, "")
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/\s+/g, " ")
|
|
119
|
+
.trim()
|
|
120
|
+
: null;
|
|
121
|
+
if (prevNorm !== null && norm === prevNorm) {
|
|
122
|
+
console.log(`[SelectorDedup] ✂️ Removed duplicate segment: "${part}"`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
deduped.push(part);
|
|
126
|
+
}
|
|
127
|
+
return deduped.join(" >> ");
|
|
128
|
+
}
|
|
129
|
+
function detectChain(lines, callerLine, oldLeafSelector) {
|
|
130
|
+
const escaped = oldLeafSelector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
131
|
+
const leafRegex = new RegExp(`\\.locator\\(["'\`]${escaped}["'\`]\\)`);
|
|
132
|
+
let leafLine = -1;
|
|
133
|
+
const scanStart = Math.max(0, callerLine - 15);
|
|
134
|
+
for (let i = callerLine; i >= scanStart; i--) {
|
|
135
|
+
if (leafRegex.test(lines[i])) {
|
|
136
|
+
leafLine = i;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (leafLine === -1)
|
|
141
|
+
return null;
|
|
142
|
+
let chainEnd = leafLine;
|
|
143
|
+
for (let i = leafLine + 1; i <= callerLine; i++) {
|
|
144
|
+
if (/^\s*\.filter\s*\(/.test(lines[i])) {
|
|
145
|
+
chainEnd = i;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let anchorSelector = "";
|
|
152
|
+
for (let i = leafLine - 1; i >= scanStart; i--) {
|
|
153
|
+
const anchorMatch = lines[i].match(/\.locator\(["'`]([^"'`]+)["'`]\)/);
|
|
154
|
+
if (anchorMatch) {
|
|
155
|
+
anchorSelector = anchorMatch[1];
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { startLine: leafLine, endLine: chainEnd, anchorSelector };
|
|
160
|
+
}
|
|
161
|
+
function detectChainRoot(lines, callerLine, oldLeafSelector) {
|
|
162
|
+
const escaped = oldLeafSelector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
163
|
+
const leafPattern = new RegExp(`(?:locator|getByRole|getByLabel|getByText|getByTestId|getByPlaceholder)\\s*\\(\\s*["'\`]${escaped}["'\`]`);
|
|
164
|
+
let leafLine = -1;
|
|
165
|
+
const scanStart = Math.max(0, callerLine - 20);
|
|
166
|
+
for (let i = callerLine; i >= scanStart; i--) {
|
|
167
|
+
if (leafPattern.test(lines[i])) {
|
|
168
|
+
leafLine = i;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (leafLine === -1)
|
|
173
|
+
return null;
|
|
174
|
+
let chainEndLine = leafLine;
|
|
175
|
+
for (let i = leafLine + 1; i <= Math.min(lines.length - 1, leafLine + 10); i++) {
|
|
176
|
+
const trimmed = lines[i].trim();
|
|
177
|
+
if (/^\.(?:filter|first|last|nth)\s*\(/.test(trimmed)) {
|
|
178
|
+
chainEndLine = i;
|
|
179
|
+
}
|
|
180
|
+
else if (trimmed.startsWith(".")) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let rootLine = leafLine;
|
|
188
|
+
let rootReceiver = "page";
|
|
189
|
+
for (let i = leafLine - 1; i >= scanStart; i--) {
|
|
190
|
+
const trimmed = lines[i].trim();
|
|
191
|
+
const combined = lines[i];
|
|
192
|
+
const isChainContinuation = trimmed.startsWith(".") ||
|
|
193
|
+
combined.trimEnd().endsWith("(") ||
|
|
194
|
+
combined.trimEnd().endsWith(",");
|
|
195
|
+
if (isChainContinuation) {
|
|
196
|
+
rootLine = i;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const assignMatch = combined.match(/(?:const|let|var)\s+[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*(page|[a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
200
|
+
if (assignMatch) {
|
|
201
|
+
rootReceiver = assignMatch[1];
|
|
202
|
+
rootLine = i;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const pageMatch = combined.match(/^\s*((?:await\s+)?(?:page|[a-zA-Z_$][a-zA-Z0-9_$]*))\s*\.(?:locator|frameLocator|getBy)/);
|
|
206
|
+
if (pageMatch) {
|
|
207
|
+
rootReceiver = pageMatch[1].replace(/^await\s+/, "").trim();
|
|
208
|
+
rootLine = i;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
if (!isChainContinuation && i < leafLine - 1)
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return { rootLine, rootReceiver, leafLine, chainEndLine };
|
|
215
|
+
}
|
|
216
|
+
// ─────────────────────────────────────────────────────────────────
|
|
217
|
+
// TEMPLATE VARIABLE PRESERVATION
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────
|
|
219
|
+
function updateTemplateLiteralPreservingVars(line, oldSelector, newSelector, domAttributeHint) {
|
|
220
|
+
if (!line.includes("`") || !line.includes("${"))
|
|
221
|
+
return null;
|
|
222
|
+
const newLeaf = newSelector.split(">>").pop().trim();
|
|
223
|
+
const varMatches = [...line.matchAll(/\$\{([^}]+)\}/g)];
|
|
224
|
+
if (varMatches.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
const varExpression = varMatches[0][1];
|
|
227
|
+
const oldAttrInTemplateMatch = line.match(/\[([a-zA-Z-]+)=["']?\$\{[^}]+\}([^"'\]`]*)["']?\]/);
|
|
228
|
+
const oldAttrName = oldAttrInTemplateMatch?.[1] ?? null;
|
|
229
|
+
const oldStaticSuffix = oldAttrInTemplateMatch?.[2] ?? null;
|
|
230
|
+
const newAttrMatch = newLeaf.match(/\[([a-zA-Z-]+)="([^"]*)"\]/);
|
|
231
|
+
const newAttrName = newAttrMatch?.[1] ?? null;
|
|
232
|
+
const newAttrValue = newAttrMatch?.[2] ?? null;
|
|
233
|
+
const oldAttrRuntimeMatch = oldSelector.match(/\[([a-zA-Z-]+)="([^"]*)"\]/);
|
|
234
|
+
const oldAttrRuntimeValue = oldAttrRuntimeMatch?.[2] ?? null;
|
|
235
|
+
let varRuntimeValue = null;
|
|
236
|
+
if (oldAttrRuntimeValue !== null && oldStaticSuffix !== null) {
|
|
237
|
+
if (oldStaticSuffix.length > 0) {
|
|
238
|
+
varRuntimeValue = oldAttrRuntimeValue.endsWith(oldStaticSuffix)
|
|
239
|
+
? oldAttrRuntimeValue.slice(0, -oldStaticSuffix.length)
|
|
240
|
+
: oldAttrRuntimeValue.split("_")[0];
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const underscoreIdx = oldAttrRuntimeValue.indexOf("_");
|
|
244
|
+
varRuntimeValue =
|
|
245
|
+
underscoreIdx >= 0
|
|
246
|
+
? oldAttrRuntimeValue.slice(0, underscoreIdx)
|
|
247
|
+
: oldAttrRuntimeValue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (oldAttrName &&
|
|
251
|
+
newAttrName &&
|
|
252
|
+
oldAttrName === newAttrName &&
|
|
253
|
+
newAttrValue) {
|
|
254
|
+
let newSuffix = "";
|
|
255
|
+
if (varRuntimeValue && newAttrValue.startsWith(varRuntimeValue)) {
|
|
256
|
+
newSuffix = newAttrValue.slice(varRuntimeValue.length);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const lastUnderscoreIdx = newAttrValue.lastIndexOf("_");
|
|
260
|
+
newSuffix =
|
|
261
|
+
lastUnderscoreIdx >= 0 ? newAttrValue.slice(lastUnderscoreIdx) : "";
|
|
262
|
+
}
|
|
263
|
+
if (newSuffix === oldStaticSuffix)
|
|
264
|
+
return null;
|
|
265
|
+
const escapedOldSuffix = (oldStaticSuffix ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
266
|
+
const suffixPattern = new RegExp(`(\\$\\{[^}]+\\})${escapedOldSuffix}(["']?\\])`);
|
|
267
|
+
const updated = line.replace(suffixPattern, `$1${newSuffix}$2`);
|
|
268
|
+
return updated !== line ? updated : null;
|
|
269
|
+
}
|
|
270
|
+
if (oldAttrName && !newAttrName) {
|
|
271
|
+
let newSuffix = "";
|
|
272
|
+
if (domAttributeHint &&
|
|
273
|
+
varRuntimeValue &&
|
|
274
|
+
domAttributeHint.startsWith(varRuntimeValue)) {
|
|
275
|
+
newSuffix = domAttributeHint.slice(varRuntimeValue.length);
|
|
276
|
+
}
|
|
277
|
+
const reconstructedAttr = newSuffix.length > 0
|
|
278
|
+
? `[${oldAttrName}="\${${varExpression}}${newSuffix}"]`
|
|
279
|
+
: `[${oldAttrName}="\${${varExpression}}"]`;
|
|
280
|
+
const oldAttrPattern = new RegExp(`\\[${oldAttrName.replace(/-/g, "\\-")}=["']?\\$\\{[^}]+\\}[^"'\\]\`]*["']?\\]`);
|
|
281
|
+
if (oldAttrPattern.test(line)) {
|
|
282
|
+
const updated = line.replace(oldAttrPattern, reconstructedAttr);
|
|
283
|
+
return updated !== line ? updated : null;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────
|
|
290
|
+
// MAIN CLASS
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────
|
|
293
|
+
// GIT HELPER (used by branch strategy and autoCommit)
|
|
294
|
+
//
|
|
295
|
+
// Design goals:
|
|
296
|
+
// • Lean — no stash, no branch-switching, no complex state management.
|
|
297
|
+
// • Commit-based — write first (inPlace), then commit. If git fails,
|
|
298
|
+
// the fix is still on disk (silent inPlace fallback).
|
|
299
|
+
// • Detached HEAD aware — skips silently in CI environments that check
|
|
300
|
+
// out a specific SHA without a branch ref.
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────
|
|
302
|
+
function runGitOperations(absoluteFilePath, createBranch, relativeFilePath) {
|
|
303
|
+
const filename = path.basename(relativeFilePath);
|
|
304
|
+
try {
|
|
305
|
+
// Bail out if HEAD is detached (CI SHA checkout, rebase mid-flight, etc.)
|
|
306
|
+
try {
|
|
307
|
+
(0, child_process_1.execSync)("git symbolic-ref HEAD", { stdio: "pipe" });
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
console.warn(`[SourceUpdater] ⚠️ Detached HEAD — git operations skipped (fix is saved to disk)`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (createBranch) {
|
|
314
|
+
const branchName = `sela/heal-${Date.now()}`;
|
|
315
|
+
(0, child_process_1.execSync)(`git checkout -b ${branchName}`, { stdio: "pipe" });
|
|
316
|
+
console.log(`[SourceUpdater] 🌿 Created branch: ${branchName}`);
|
|
317
|
+
}
|
|
318
|
+
(0, child_process_1.execSync)(`git add "${absoluteFilePath}"`, { stdio: "pipe" });
|
|
319
|
+
(0, child_process_1.execSync)(`git commit -m "fix(sela): heal selector in ${filename}"`, {
|
|
320
|
+
stdio: "pipe",
|
|
321
|
+
});
|
|
322
|
+
console.log(`[SourceUpdater] ✅ Auto-committed fix in: ${filename}`);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
console.warn(`[SourceUpdater] ⚠️ Git operation failed: ${err.message} — fix written to disk (inPlace fallback)`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
class SourceUpdater {
|
|
329
|
+
// ─────────────────────────────────────────────────────────────
|
|
330
|
+
// PUBLIC ENTRY POINT
|
|
331
|
+
// Handles commentOnly short-circuit and post-write git operations,
|
|
332
|
+
// then delegates all selector-rewriting work to _coreUpdate().
|
|
333
|
+
// ─────────────────────────────────────────────────────────────
|
|
334
|
+
static update(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint, updateStrategy, autoCommit) {
|
|
335
|
+
const resolvedPath = SourceUpdater.resolveFilePath(caller.filePath);
|
|
336
|
+
// ── commentOnly: write a TODO comment, skip all AST/regex work ──
|
|
337
|
+
if (updateStrategy === "commentOnly") {
|
|
338
|
+
if (!resolvedPath) {
|
|
339
|
+
return { success: false, reason: `File not found: ${caller.filePath}` };
|
|
340
|
+
}
|
|
341
|
+
const effective = deduplicateRuntimeSelector(newSelector);
|
|
342
|
+
return SourceUpdater.applyCommentOnly(caller, oldSelector, effective, resolvedPath);
|
|
343
|
+
}
|
|
344
|
+
// ── inPlace / branch: run the full rewriting pipeline ──────────
|
|
345
|
+
const result = SourceUpdater._coreUpdate(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint);
|
|
346
|
+
// ── Post-write git operations (branch or autoCommit) ───────────
|
|
347
|
+
if (result.success &&
|
|
348
|
+
resolvedPath &&
|
|
349
|
+
(updateStrategy === "branch" || autoCommit)) {
|
|
350
|
+
runGitOperations(resolvedPath, updateStrategy === "branch", caller.filePath);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
// ─────────────────────────────────────────────────────────────
|
|
355
|
+
// commentOnly helper — inserts a TODO comment above the failing
|
|
356
|
+
// call site and returns success:false so the engine knows the
|
|
357
|
+
// source was NOT rewritten (no retry suppression needed).
|
|
358
|
+
// ─────────────────────────────────────────────────────────────
|
|
359
|
+
static applyCommentOnly(caller, oldSelector, newSelector, filePath) {
|
|
360
|
+
try {
|
|
361
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
362
|
+
const lines = raw.split(/\r?\n/);
|
|
363
|
+
const lineIdx = caller.line - 1; // 0-based
|
|
364
|
+
if (lineIdx < 0 || lineIdx >= lines.length) {
|
|
365
|
+
return { success: false, reason: "COMMENT_ONLY: line out of range" };
|
|
366
|
+
}
|
|
367
|
+
const indent = lines[lineIdx].match(/^(\s*)/)?.[1] ?? "";
|
|
368
|
+
const comment = `${indent}// TODO [Sela]: replace "${oldSelector}" with "${newSelector}"`;
|
|
369
|
+
lines.splice(lineIdx, 0, comment);
|
|
370
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
371
|
+
console.log(`[SourceUpdater] 💬 Comment-only: inserted TODO at line ${caller.line} in ${path.basename(filePath)}`);
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
reason: "COMMENT_ONLY: TODO inserted, source selector not modified",
|
|
375
|
+
healedLocatorString: newSelector,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
reason: `COMMENT_ONLY: write failed — ${err.message}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// ─────────────────────────────────────────────────────────────
|
|
386
|
+
// CORE UPDATE — Layered Healing (AST primary, Regex fallback)
|
|
387
|
+
// Called by update(); never call directly from outside the class.
|
|
388
|
+
// ─────────────────────────────────────────────────────────────
|
|
389
|
+
static _coreUpdate(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint) {
|
|
390
|
+
const filePath = SourceUpdater.resolveFilePath(caller.filePath);
|
|
391
|
+
// ── FIX 2B: Deduplicate the runtime selector BEFORE passing it down ──
|
|
392
|
+
// This prevents the engine from writing double-frame selectors like
|
|
393
|
+
// "#modern-frame-container >> #modern-frame-container >> ..."
|
|
394
|
+
const deduplicatedNewSelector = deduplicateRuntimeSelector(newSelector);
|
|
395
|
+
if (deduplicatedNewSelector !== newSelector) {
|
|
396
|
+
console.log(`[SourceUpdater] ✂️ Runtime selector deduplicated:\n` +
|
|
397
|
+
` before: "${newSelector}"\n` +
|
|
398
|
+
` after: "${deduplicatedNewSelector}"`);
|
|
399
|
+
}
|
|
400
|
+
// Use the deduplicated selector for all subsequent operations
|
|
401
|
+
const effectiveNewSelector = deduplicatedNewSelector;
|
|
402
|
+
console.log(`[SourceUpdater] 📂 Attempting update in file: ${filePath}`);
|
|
403
|
+
console.log(`[SourceUpdater] 🔄 Request: "${oldSelector}" -> "${effectiveNewSelector}" (Line: ${caller.line})`);
|
|
404
|
+
if (smartChainSegments?.length) {
|
|
405
|
+
console.log(`[SourceUpdater] 🔗 smartChain: ${smartChainSegments.length} segments, ` +
|
|
406
|
+
`hint: ${originalChainHint?.length ?? 0} segments`);
|
|
407
|
+
}
|
|
408
|
+
if (contentChange) {
|
|
409
|
+
console.log(`[SourceUpdater] 📝 contentChange: "${contentChange.oldText}" → "${contentChange.newText}"`);
|
|
410
|
+
}
|
|
411
|
+
if (!filePath) {
|
|
412
|
+
console.error(`[SourceUpdater] ❌ File not found: ${caller.filePath}`);
|
|
413
|
+
return { success: false, reason: `File not found: ${caller.filePath}` };
|
|
414
|
+
}
|
|
415
|
+
// ── 1. AST Engine (Primary) ─────────────────────────────────────
|
|
416
|
+
try {
|
|
417
|
+
const astResult = astUpdater.update(caller, oldSelector, effectiveNewSelector, aiSegments, fullSelectorContext, contentChange, smartChainSegments, originalChainHint);
|
|
418
|
+
if (astResult.success) {
|
|
419
|
+
console.log(`[SourceUpdater] ✅ AST Strategy ${astResult.strategy} SUCCESS` +
|
|
420
|
+
(astResult.lineUpdated !== undefined
|
|
421
|
+
? ` — line ${astResult.lineUpdated + 1}`
|
|
422
|
+
: ""));
|
|
423
|
+
return {
|
|
424
|
+
success: true,
|
|
425
|
+
reason: `AST[${astResult.strategy}]: ${astResult.reason}`,
|
|
426
|
+
lineUpdated: astResult.lineUpdated,
|
|
427
|
+
// Prefer the AST's own healedLocatorString; fall back to the deduplicated input.
|
|
428
|
+
healedLocatorString: astResult.healedLocatorString ?? effectiveNewSelector,
|
|
429
|
+
definitionSite: astResult.definitionSite,
|
|
430
|
+
blastRadius: astResult.blastRadius,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Semantic guard means we identified a definition-site shape (IDENTIFIER /
|
|
434
|
+
// PROPERTY_ACCESS / TEMPLATE_LITERAL) but could not safely resolve the new
|
|
435
|
+
// value. Regex cannot do better and will corrupt the definition site.
|
|
436
|
+
if (astResult.strategy === "semantic-guard") {
|
|
437
|
+
console.warn(`[SourceUpdater] 🛑 Regex blocked — semantic guard fired. ` +
|
|
438
|
+
`Source unchanged. Reason: ${astResult.reason}`);
|
|
439
|
+
return { success: false, reason: astResult.reason };
|
|
440
|
+
}
|
|
441
|
+
console.log(`[SourceUpdater] ⚠️ AST engine exhausted (${astResult.reason}), falling back to Regex`);
|
|
442
|
+
}
|
|
443
|
+
catch (astError) {
|
|
444
|
+
console.warn(`[SourceUpdater] ⚠️ AST engine error: ${astError.message}. Falling back to Regex`);
|
|
445
|
+
}
|
|
446
|
+
// ── 2. Regex Fallback ──────────────────────────────────────────
|
|
447
|
+
console.log(`[SourceUpdater] 🔄 Using Regex fallback strategies...`);
|
|
448
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
449
|
+
const lines = raw.split(/\r?\n/);
|
|
450
|
+
if (caller.line <= 0) {
|
|
451
|
+
console.log(`[SourceUpdater] ⚠️ line ${caller.line} invalid — running Regex Strategy G only`);
|
|
452
|
+
if (contentChange) {
|
|
453
|
+
const assertResult = SourceUpdater.strategyAssertionHealing(lines, 1, contentChange, true);
|
|
454
|
+
if (assertResult.success) {
|
|
455
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
456
|
+
console.log(`[SourceUpdater] ✅ Regex Strategy G (Assertion Healing) SUCCESS`);
|
|
457
|
+
return assertResult;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
console.warn(`[SourceUpdater] ❌ ALL STRATEGIES FAILED for line-0 heal`);
|
|
461
|
+
return { success: false, reason: "invalid line number (≤0)" };
|
|
462
|
+
}
|
|
463
|
+
const callerLine = caller.line - 1; // 0-based
|
|
464
|
+
if (!lines[callerLine]) {
|
|
465
|
+
console.warn(`[SourceUpdater] 📍 Target line ${caller.line} is empty or out of bounds.`);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
console.log(`[SourceUpdater] 📍 Analyzing line ${caller.line}: "${lines[callerLine].trim()}"`);
|
|
469
|
+
}
|
|
470
|
+
const tryAssertionHealing = () => {
|
|
471
|
+
if (!contentChange)
|
|
472
|
+
return;
|
|
473
|
+
console.log(`[SourceUpdater] 🔍 Checking for assertion mismatches...`);
|
|
474
|
+
console.warn(`\n[Fixwright-Advice] 💡 To fix the assertion at line ${caller.line}, ` +
|
|
475
|
+
`change to: .toHaveText("${contentChange.newText}")\n`);
|
|
476
|
+
};
|
|
477
|
+
// ── Strategy 0: Layered Frame Healing ──────────────────────
|
|
478
|
+
if (aiSegments && aiSegments.length > 0 && fullSelectorContext) {
|
|
479
|
+
const layeredResult = SourceUpdater.strategyLayeredHealing(lines, callerLine, oldSelector, aiSegments, fullSelectorContext);
|
|
480
|
+
if (layeredResult.success) {
|
|
481
|
+
tryAssertionHealing();
|
|
482
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
483
|
+
console.log(`[SourceUpdater] ✅ Strategy 0 (Layered Healing) SUCCESS — line ${layeredResult.lineUpdated + 1}`);
|
|
484
|
+
return layeredResult;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// ── Strategy F: Chain Collapsing (Regex) ───────────────────
|
|
488
|
+
const chainCollapseResult = SourceUpdater.strategyChainCollapse_Regex(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
489
|
+
if (chainCollapseResult.success) {
|
|
490
|
+
tryAssertionHealing();
|
|
491
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
492
|
+
console.log(`[SourceUpdater] ✅ Strategy F (Chain Collapse) SUCCESS`);
|
|
493
|
+
return chainCollapseResult;
|
|
494
|
+
}
|
|
495
|
+
// ── Strategy 1: Chain Collapse (original leaf-only) ────────
|
|
496
|
+
const chainResult = SourceUpdater.strategyChainCollapse(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
497
|
+
if (chainResult.success) {
|
|
498
|
+
tryAssertionHealing();
|
|
499
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
500
|
+
console.log(`[SourceUpdater] ✅ Strategy 1 (Chain Collapse) SUCCESS`);
|
|
501
|
+
return chainResult;
|
|
502
|
+
}
|
|
503
|
+
// ── Strategy 2: Direct Literal ─────────────────────────────
|
|
504
|
+
const directResult = SourceUpdater.strategyDirectLiteral(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
505
|
+
if (directResult.success) {
|
|
506
|
+
tryAssertionHealing();
|
|
507
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
508
|
+
console.log(`[SourceUpdater] ✅ Strategy 2 (Direct Literal) SUCCESS`);
|
|
509
|
+
return directResult;
|
|
510
|
+
}
|
|
511
|
+
// ── Strategy 3: Upstream Variable ──────────────────────────
|
|
512
|
+
const varResult = SourceUpdater.strategyUpstreamVariable(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
513
|
+
if (varResult.success) {
|
|
514
|
+
tryAssertionHealing();
|
|
515
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
516
|
+
console.log(`[SourceUpdater] ✅ Strategy 3 (Upstream Variable) SUCCESS`);
|
|
517
|
+
return varResult;
|
|
518
|
+
}
|
|
519
|
+
// ── Strategy 4: Chained Dedup ──────────────────────────────
|
|
520
|
+
const chainDedupResult = SourceUpdater.strategyChainedLocator(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
521
|
+
if (chainDedupResult.success) {
|
|
522
|
+
tryAssertionHealing();
|
|
523
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
524
|
+
console.log(`[SourceUpdater] ✅ Strategy 4 (Chained Dedup) SUCCESS`);
|
|
525
|
+
return chainDedupResult;
|
|
526
|
+
}
|
|
527
|
+
// ── Strategy 5: Function Return ────────────────────────────
|
|
528
|
+
const fnResult = SourceUpdater.strategyFunctionReturn(lines, callerLine, oldSelector, effectiveNewSelector, domContext, dnaAttrHint);
|
|
529
|
+
if (fnResult.success) {
|
|
530
|
+
tryAssertionHealing();
|
|
531
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
532
|
+
console.log(`[SourceUpdater] ✅ Strategy 5 (Function Return) SUCCESS`);
|
|
533
|
+
return fnResult;
|
|
534
|
+
}
|
|
535
|
+
// ── Strategy 6: Global Scan ────────────────────────────────
|
|
536
|
+
const scanResult = SourceUpdater.strategyGlobalScan(lines, oldSelector, effectiveNewSelector);
|
|
537
|
+
if (scanResult.success) {
|
|
538
|
+
tryAssertionHealing();
|
|
539
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
540
|
+
console.log(`[SourceUpdater] ✅ Strategy 6 (Global Scan) SUCCESS`);
|
|
541
|
+
return scanResult;
|
|
542
|
+
}
|
|
543
|
+
// ── Strategy G: Assertion Healing standalone ───────────────
|
|
544
|
+
if (contentChange) {
|
|
545
|
+
const assertResult = SourceUpdater.strategyAssertionHealing(lines, callerLine, contentChange);
|
|
546
|
+
if (assertResult.success) {
|
|
547
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
548
|
+
console.log(`[SourceUpdater] ✅ Strategy G (Assertion Healing) SUCCESS`);
|
|
549
|
+
return assertResult;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
console.warn(`[SourceUpdater] ❌ ALL STRATEGIES FAILED for: "${oldSelector}"`);
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
reason: "No strategy matched (including AST and Regex Fallbacks)",
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// ─────────────────────────────────────────────────────────────
|
|
559
|
+
// STRATEGY F (Regex) — Full Chain Collapsing
|
|
560
|
+
// ─────────────────────────────────────────────────────────────
|
|
561
|
+
static strategyChainCollapse_Regex(lines, callerLine, oldSelector, newSelector) {
|
|
562
|
+
console.log(`[Strategy-F] ⛓️ Starting Regex Chain Collapse...`);
|
|
563
|
+
const oldLeaf = oldSelector.split(">>").pop().trim();
|
|
564
|
+
const chainRootInfo = detectChainRoot(lines, callerLine, oldLeaf);
|
|
565
|
+
if (!chainRootInfo) {
|
|
566
|
+
return { success: false, reason: "no chain root detected" };
|
|
567
|
+
}
|
|
568
|
+
const { rootLine, rootReceiver, leafLine, chainEndLine } = chainRootInfo;
|
|
569
|
+
if (rootLine === leafLine && chainEndLine === leafLine) {
|
|
570
|
+
return { success: false, reason: "single-line call, not a chain" };
|
|
571
|
+
}
|
|
572
|
+
console.log(`[Strategy-F] 🔗 Chain detected: root=${rootLine + 1}, leaf=${leafLine + 1}, ` +
|
|
573
|
+
`end=${chainEndLine + 1}, receiver="${rootReceiver}"`);
|
|
574
|
+
const newLeaf = newSelector.split(">>").pop().trim();
|
|
575
|
+
const safeNewLeaf = newLeaf.replace(/"/g, "'");
|
|
576
|
+
const newCallText = `.locator("${safeNewLeaf}")`;
|
|
577
|
+
const indentMatch = lines[rootLine].match(/^(\s*)/);
|
|
578
|
+
const indent = indentMatch ? indentMatch[1] : "";
|
|
579
|
+
const newLine = `${indent}${rootReceiver}${newCallText}`;
|
|
580
|
+
if (rootLine !== chainEndLine) {
|
|
581
|
+
const linesToReplace = chainEndLine - rootLine + 1;
|
|
582
|
+
lines.splice(rootLine, linesToReplace, newLine);
|
|
583
|
+
console.log(`[Strategy-F] ✅ Multi-line chain collapsed (${linesToReplace} lines → 1): "${newLine.trim()}"`);
|
|
584
|
+
return {
|
|
585
|
+
success: true,
|
|
586
|
+
reason: `chain collapsed (${linesToReplace} lines → 1)`,
|
|
587
|
+
lineUpdated: rootLine,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
const originalLine = lines[rootLine];
|
|
592
|
+
const receiverPattern = new RegExp(`(${rootReceiver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})` +
|
|
593
|
+
`(?:\\s*\\.\\s*(?:locator|getBy\\w+|frameLocator|filter|first|last|nth)\\s*\\([^)]*\\))+`);
|
|
594
|
+
if (receiverPattern.test(originalLine)) {
|
|
595
|
+
const replaced = originalLine.replace(receiverPattern, `${rootReceiver}${newCallText}`);
|
|
596
|
+
if (replaced !== originalLine) {
|
|
597
|
+
lines[rootLine] = replaced;
|
|
598
|
+
console.log(`[Strategy-F] ✅ Single-line chain collapsed: "${replaced.trim()}"`);
|
|
599
|
+
return {
|
|
600
|
+
success: true,
|
|
601
|
+
reason: "single-line chain collapsed",
|
|
602
|
+
lineUpdated: rootLine,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { success: false, reason: "chain detected but could not replace" };
|
|
608
|
+
}
|
|
609
|
+
// ─────────────────────────────────────────────────────────────
|
|
610
|
+
// STRATEGY G (Regex) — Assertion Healing
|
|
611
|
+
// ─────────────────────────────────────────────────────────────
|
|
612
|
+
static strategyAssertionHealing(lines, callerLine, contentChange, wholeFile = false) {
|
|
613
|
+
const { oldText, newText } = contentChange;
|
|
614
|
+
console.log(`[Strategy-G] 🩹 Assertion Healing: "${oldText}" → "${newText}"`);
|
|
615
|
+
const TEXT_MATCHERS = [
|
|
616
|
+
"toHaveText",
|
|
617
|
+
"toContainText",
|
|
618
|
+
"toHaveValue",
|
|
619
|
+
"toHaveAttribute",
|
|
620
|
+
"toHaveLabel",
|
|
621
|
+
"toHaveTitle",
|
|
622
|
+
];
|
|
623
|
+
const matcherPattern = new RegExp(`\\.(?:${TEXT_MATCHERS.join("|")})\\s*\\(`);
|
|
624
|
+
const searchStart = wholeFile ? 0 : Math.max(0, callerLine - 2);
|
|
625
|
+
const searchEnd = wholeFile
|
|
626
|
+
? lines.length - 1
|
|
627
|
+
: Math.min(lines.length - 1, callerLine + 40);
|
|
628
|
+
let healedCount = 0;
|
|
629
|
+
let lastHealed;
|
|
630
|
+
for (let i = searchStart; i <= searchEnd; i++) {
|
|
631
|
+
const line = lines[i];
|
|
632
|
+
if (!matcherPattern.test(line))
|
|
633
|
+
continue;
|
|
634
|
+
for (const q of [`"`, `'`, "`"]) {
|
|
635
|
+
const escapedOld = oldText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
636
|
+
const valuePattern = new RegExp(`${q}([^${q}]*${escapedOld}[^${q}]*)${q}`);
|
|
637
|
+
const match = valuePattern.exec(line);
|
|
638
|
+
if (match) {
|
|
639
|
+
const oldValue = match[1];
|
|
640
|
+
const newValue = oldValue.replace(oldText, newText);
|
|
641
|
+
const safeNew = sanitizeForInjection(newValue, q);
|
|
642
|
+
const escaped = oldValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
643
|
+
const replaceRx = new RegExp(`${q}${escaped}${q}`);
|
|
644
|
+
const newLine = line.replace(replaceRx, `${q}${safeNew}${q}`);
|
|
645
|
+
if (newLine !== line) {
|
|
646
|
+
lines[i] = newLine;
|
|
647
|
+
healedCount++;
|
|
648
|
+
lastHealed = i;
|
|
649
|
+
console.log(`[Strategy-G] ✅ Healed assertion at line ${i + 1}: "${oldValue}" → "${newValue}"`);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (healedCount > 0) {
|
|
656
|
+
return {
|
|
657
|
+
success: true,
|
|
658
|
+
reason: `assertion healing: ${healedCount} assertion(s) updated`,
|
|
659
|
+
lineUpdated: lastHealed,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return { success: false, reason: "no assertions to heal" };
|
|
663
|
+
}
|
|
664
|
+
// ─────────────────────────────────────────────────────────────
|
|
665
|
+
// STRATEGY 0 — Layered Healing (Upstream Variable Tracing)
|
|
666
|
+
// ─────────────────────────────────────────────────────────────
|
|
667
|
+
static strategyLayeredHealing(lines, callerLine, oldElementSelector, aiSegments, fullSelectorContext) {
|
|
668
|
+
console.log(`[SourceUpdater] 🧠 Strategy 0: Starting Layered Healing...`);
|
|
669
|
+
const failureLine = lines[callerLine] ?? "";
|
|
670
|
+
const varUsageMatch = failureLine.match(/^\s*(?:await\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*locator\s*\(/);
|
|
671
|
+
const usedVarName = varUsageMatch?.[1];
|
|
672
|
+
if (usedVarName && usedVarName !== "page") {
|
|
673
|
+
const origin = SourceUpdater.findVariableOrigin(lines, usedVarName, callerLine);
|
|
674
|
+
if (origin) {
|
|
675
|
+
const frameSegments = aiSegments.filter((s) => s.type === "frame");
|
|
676
|
+
if (frameSegments.length === 0) {
|
|
677
|
+
lines[callerLine] = failureLine.replace(`${usedVarName}.locator`, `page.locator`);
|
|
678
|
+
return {
|
|
679
|
+
success: true,
|
|
680
|
+
reason: `Collapsed variable '${usedVarName}' to 'page'`,
|
|
681
|
+
lineUpdated: callerLine,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
let newDeclaration = ` const ${usedVarName} = page`;
|
|
685
|
+
frameSegments.forEach((seg) => {
|
|
686
|
+
newDeclaration += `.frameLocator("${seg.selector}")`;
|
|
687
|
+
});
|
|
688
|
+
newDeclaration += ";";
|
|
689
|
+
const scanEnd = Math.min(lines.length - 1, origin.declarationLine + 5);
|
|
690
|
+
let endOfDeclaration = origin.declarationLine;
|
|
691
|
+
for (let i = origin.declarationLine; i <= scanEnd; i++) {
|
|
692
|
+
if (lines[i].includes(";")) {
|
|
693
|
+
endOfDeclaration = i;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
lines.splice(origin.declarationLine, endOfDeclaration - origin.declarationLine + 1, newDeclaration);
|
|
698
|
+
return {
|
|
699
|
+
success: true,
|
|
700
|
+
reason: `Reconstructed variable '${usedVarName}' with AI frame path`,
|
|
701
|
+
lineUpdated: origin.declarationLine,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return { success: false, reason: "no matches found in chain" };
|
|
706
|
+
}
|
|
707
|
+
static findVariableOrigin(lines, varName, startLine) {
|
|
708
|
+
const scanStart = Math.max(0, startLine - 30);
|
|
709
|
+
const declarationPattern = new RegExp(`^\\s*(?:const|let|var)\\s+${varName}\\s*=`);
|
|
710
|
+
for (let i = startLine - 1; i >= scanStart; i--) {
|
|
711
|
+
if (declarationPattern.test(lines[i])) {
|
|
712
|
+
return { varName, declarationLine: i, lineContent: lines[i] };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
// ─────────────────────────────────────────────────────────────
|
|
718
|
+
// STRATEGY 1 — Chain Collapse (leaf-only precision replacement)
|
|
719
|
+
// ─────────────────────────────────────────────────────────────
|
|
720
|
+
static strategyChainCollapse(lines, callerLine, oldSelector, newSelector) {
|
|
721
|
+
const oldLeaf = oldSelector
|
|
722
|
+
.split(">>")
|
|
723
|
+
.pop()
|
|
724
|
+
.replace(/\.filter\([^)]*\)/g, "")
|
|
725
|
+
.trim();
|
|
726
|
+
const newLeaf = newSelector.split(">>").pop().trim();
|
|
727
|
+
const chain = detectChain(lines, callerLine, oldLeaf);
|
|
728
|
+
if (!chain)
|
|
729
|
+
return { success: false, reason: "no chain detected" };
|
|
730
|
+
const deduped = chain.anchorSelector
|
|
731
|
+
? deduplicateSelector(newSelector, chain.anchorSelector)
|
|
732
|
+
: newLeaf;
|
|
733
|
+
const outerQuote = detectOuterQuote(lines[chain.startLine], oldLeaf) ?? `"`;
|
|
734
|
+
const safeDeduped = sanitizeForInjection(deduped, outerQuote);
|
|
735
|
+
const escapedOldLeaf = oldLeaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
736
|
+
const escapedQ = outerQuote.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
737
|
+
const leafLiteralRegex = new RegExp(`(\\.locator\\(${escapedQ})${escapedOldLeaf}(${escapedQ}\\))`);
|
|
738
|
+
if (leafLiteralRegex.test(lines[chain.startLine])) {
|
|
739
|
+
lines[chain.startLine] = lines[chain.startLine].replace(leafLiteralRegex, `$1${safeDeduped}$2`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
const replaced = replaceStringLiteralInLine(lines[chain.startLine], oldLeaf, safeDeduped);
|
|
743
|
+
if (replaced === null)
|
|
744
|
+
return { success: false, reason: "chain leaf literal not found" };
|
|
745
|
+
lines[chain.startLine] = replaced;
|
|
746
|
+
}
|
|
747
|
+
if (chain.endLine > chain.startLine) {
|
|
748
|
+
for (let i = chain.endLine; i > chain.startLine; i--) {
|
|
749
|
+
if (/^\s*\.filter\s*\(/.test(lines[i]))
|
|
750
|
+
lines.splice(i, 1);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
success: true,
|
|
755
|
+
reason: `chain collapsed: '${oldLeaf}' → '${deduped}'`,
|
|
756
|
+
lineUpdated: chain.startLine,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
// ─────────────────────────────────────────────────────────────
|
|
760
|
+
// STRATEGY 2 — Direct Literal
|
|
761
|
+
// ─────────────────────────────────────────────────────────────
|
|
762
|
+
static strategyDirectLiteral(lines, callerLine, oldSelector, newSelector) {
|
|
763
|
+
const windowSize = 10;
|
|
764
|
+
const start = Math.max(0, callerLine - windowSize);
|
|
765
|
+
const end = Math.min(lines.length - 1, callerLine + windowSize);
|
|
766
|
+
const lastOldPart = oldSelector.split(">>").pop().trim();
|
|
767
|
+
const lastNewPart = newSelector.split(">>").pop().trim();
|
|
768
|
+
for (let i = callerLine; i >= start; i--) {
|
|
769
|
+
const outerQ = detectOuterQuote(lines[i], lastOldPart);
|
|
770
|
+
if (!outerQ)
|
|
771
|
+
continue;
|
|
772
|
+
const safe = sanitizeForInjection(lastNewPart, outerQ);
|
|
773
|
+
const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
|
|
774
|
+
if (replaced !== null) {
|
|
775
|
+
lines[i] = replaced;
|
|
776
|
+
return { success: true, reason: "direct literal", lineUpdated: i };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
for (let i = callerLine + 1; i <= end; i++) {
|
|
780
|
+
const outerQ = detectOuterQuote(lines[i], lastOldPart);
|
|
781
|
+
if (!outerQ)
|
|
782
|
+
continue;
|
|
783
|
+
const safe = sanitizeForInjection(lastNewPart, outerQ);
|
|
784
|
+
const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
|
|
785
|
+
if (replaced !== null) {
|
|
786
|
+
lines[i] = replaced;
|
|
787
|
+
return { success: true, reason: "direct literal", lineUpdated: i };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return { success: false, reason: "no direct literal" };
|
|
791
|
+
}
|
|
792
|
+
// ─────────────────────────────────────────────────────────────
|
|
793
|
+
// STRATEGY 3 — Upstream Variable/Const
|
|
794
|
+
// ─────────────────────────────────────────────────────────────
|
|
795
|
+
static strategyUpstreamVariable(lines, callerLine, oldSelector, newSelector) {
|
|
796
|
+
const callBlock = lines
|
|
797
|
+
.slice(Math.max(0, callerLine - 5), callerLine + 1)
|
|
798
|
+
.join("\n");
|
|
799
|
+
const identifierPattern = /\b([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*)\b/g;
|
|
800
|
+
const candidates = new Set();
|
|
801
|
+
let m;
|
|
802
|
+
while ((m = identifierPattern.exec(callBlock)) !== null)
|
|
803
|
+
candidates.add(m[1]);
|
|
804
|
+
const lastOldPart = oldSelector.split(">>").pop().trim();
|
|
805
|
+
const lastNewPart = newSelector.split(">>").pop().trim();
|
|
806
|
+
for (let i = 0; i < lines.length; i++) {
|
|
807
|
+
const line = lines[i];
|
|
808
|
+
const outerQ = detectOuterQuote(line, lastOldPart);
|
|
809
|
+
if (!outerQ)
|
|
810
|
+
continue;
|
|
811
|
+
const safe = sanitizeForInjection(lastNewPart, outerQ);
|
|
812
|
+
const replaced = replaceStringLiteralInLine(line, lastOldPart, safe);
|
|
813
|
+
if (replaced === null)
|
|
814
|
+
continue;
|
|
815
|
+
for (const ident of candidates) {
|
|
816
|
+
const baseName = ident.split(".")[0];
|
|
817
|
+
if (line.includes(baseName)) {
|
|
818
|
+
lines[i] = replaced;
|
|
819
|
+
return {
|
|
820
|
+
success: true,
|
|
821
|
+
reason: `upstream variable '${ident}'`,
|
|
822
|
+
lineUpdated: i,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return { success: false, reason: "no upstream variable" };
|
|
828
|
+
}
|
|
829
|
+
// ─────────────────────────────────────────────────────────────
|
|
830
|
+
// STRATEGY 4 — Chained Locator Dedup
|
|
831
|
+
// ─────────────────────────────────────────────────────────────
|
|
832
|
+
static strategyChainedLocator(lines, callerLine, oldSelector, newSelector) {
|
|
833
|
+
const blockStart = Math.max(0, callerLine - 10);
|
|
834
|
+
const chainBlock = lines.slice(blockStart, callerLine + 1).join("\n");
|
|
835
|
+
const deduped = deduplicateSelector(newSelector, oldSelector + "\n" + chainBlock);
|
|
836
|
+
if (deduped === newSelector)
|
|
837
|
+
return { success: false, reason: "no duplication detected" };
|
|
838
|
+
const lastOldPart = oldSelector.split(">>").pop().trim();
|
|
839
|
+
for (let i = callerLine; i >= blockStart; i--) {
|
|
840
|
+
const outerQ = detectOuterQuote(lines[i], lastOldPart);
|
|
841
|
+
if (!outerQ)
|
|
842
|
+
continue;
|
|
843
|
+
const safe = sanitizeForInjection(deduped, outerQ);
|
|
844
|
+
const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
|
|
845
|
+
if (replaced !== null) {
|
|
846
|
+
lines[i] = replaced;
|
|
847
|
+
return {
|
|
848
|
+
success: true,
|
|
849
|
+
reason: `chained dedup: '${newSelector}' → '${deduped}'`,
|
|
850
|
+
lineUpdated: i,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return { success: false, reason: "dedup found but literal not replaced" };
|
|
855
|
+
}
|
|
856
|
+
// ─────────────────────────────────────────────────────────────
|
|
857
|
+
// STRATEGY 5 — Function/Template with Variable Preservation
|
|
858
|
+
// ─────────────────────────────────────────────────────────────
|
|
859
|
+
static strategyFunctionReturn(lines, callerLine, oldSelector, newSelector, domContext, dnaAttrHint) {
|
|
860
|
+
const callBlock = lines
|
|
861
|
+
.slice(Math.max(0, callerLine - 8), callerLine + 1)
|
|
862
|
+
.join("\n");
|
|
863
|
+
const fnCallPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
864
|
+
const fnNames = new Set();
|
|
865
|
+
let m;
|
|
866
|
+
const playwrightBuiltins = new Set([
|
|
867
|
+
"locator",
|
|
868
|
+
"fill",
|
|
869
|
+
"click",
|
|
870
|
+
"check",
|
|
871
|
+
"uncheck",
|
|
872
|
+
"hover",
|
|
873
|
+
"dblclick",
|
|
874
|
+
"focus",
|
|
875
|
+
"waitFor",
|
|
876
|
+
"isVisible",
|
|
877
|
+
"getAttribute",
|
|
878
|
+
"innerText",
|
|
879
|
+
"innerHTML",
|
|
880
|
+
"textContent",
|
|
881
|
+
"page",
|
|
882
|
+
"filter",
|
|
883
|
+
"expect",
|
|
884
|
+
"selectOption",
|
|
885
|
+
"frameLocator",
|
|
886
|
+
]);
|
|
887
|
+
while ((m = fnCallPattern.exec(callBlock)) !== null) {
|
|
888
|
+
if (!playwrightBuiltins.has(m[1]))
|
|
889
|
+
fnNames.add(m[1]);
|
|
890
|
+
}
|
|
891
|
+
if (fnNames.size === 0)
|
|
892
|
+
return { success: false, reason: "no function calls found near site" };
|
|
893
|
+
const newLeaf = newSelector.split(">>").pop().trim();
|
|
894
|
+
const oldSelectorAttrMatch = oldSelector.match(/\[([a-zA-Z][a-zA-Z0-9-]*)=/);
|
|
895
|
+
const expectedAttrName = oldSelectorAttrMatch?.[1] ?? null;
|
|
896
|
+
const resolvedAttrHint = dnaAttrHint ??
|
|
897
|
+
SourceUpdater.extractAttrValueFromDom(domContext ?? "", oldSelector) ??
|
|
898
|
+
undefined;
|
|
899
|
+
for (const fnName of fnNames) {
|
|
900
|
+
const defPattern = new RegExp(`(?:const|let|var|function)\\s+${fnName}\\b`);
|
|
901
|
+
for (let i = 0; i < lines.length; i++) {
|
|
902
|
+
if (!defPattern.test(lines[i]))
|
|
903
|
+
continue;
|
|
904
|
+
for (let j = i; j < Math.min(lines.length, i + 8); j++) {
|
|
905
|
+
const line = lines[j];
|
|
906
|
+
if (line.includes("`") && line.includes("${")) {
|
|
907
|
+
if (expectedAttrName && !line.includes(expectedAttrName))
|
|
908
|
+
continue;
|
|
909
|
+
if (!expectedAttrName)
|
|
910
|
+
continue;
|
|
911
|
+
const preserved = updateTemplateLiteralPreservingVars(line, oldSelector, newSelector, resolvedAttrHint);
|
|
912
|
+
if (preserved !== null && preserved !== line) {
|
|
913
|
+
lines[j] = preserved;
|
|
914
|
+
return {
|
|
915
|
+
success: true,
|
|
916
|
+
reason: `function template updated (vars preserved)`,
|
|
917
|
+
lineUpdated: j,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const lastOldPart = oldSelector.split(">>").pop().trim();
|
|
923
|
+
const outerQ = detectOuterQuote(line, lastOldPart);
|
|
924
|
+
if (!outerQ)
|
|
925
|
+
continue;
|
|
926
|
+
const safe = sanitizeForInjection(newLeaf, outerQ);
|
|
927
|
+
const replaced = replaceStringLiteralInLine(line, lastOldPart, safe);
|
|
928
|
+
if (replaced !== null) {
|
|
929
|
+
lines[j] = replaced;
|
|
930
|
+
return {
|
|
931
|
+
success: true,
|
|
932
|
+
reason: `function literal replaced`,
|
|
933
|
+
lineUpdated: j,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return { success: false, reason: "function strategy exhausted" };
|
|
940
|
+
}
|
|
941
|
+
// ─────────────────────────────────────────────────────────────
|
|
942
|
+
// STRATEGY 6 — Global Scan
|
|
943
|
+
// ─────────────────────────────────────────────────────────────
|
|
944
|
+
static strategyGlobalScan(lines, oldSelector, newSelector) {
|
|
945
|
+
const lastOldPart = oldSelector.split(">>").pop().trim();
|
|
946
|
+
const lastNewPart = newSelector.split(">>").pop().trim();
|
|
947
|
+
for (let i = 0; i < lines.length; i++) {
|
|
948
|
+
const deduped = deduplicateSelector(newSelector, lines[i]);
|
|
949
|
+
const effectiveNew = deduped !== newSelector ? deduped : lastNewPart;
|
|
950
|
+
const outerQ = detectOuterQuote(lines[i], lastOldPart);
|
|
951
|
+
if (!outerQ)
|
|
952
|
+
continue;
|
|
953
|
+
const safe = sanitizeForInjection(effectiveNew, outerQ);
|
|
954
|
+
const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
|
|
955
|
+
if (replaced !== null) {
|
|
956
|
+
lines[i] = replaced;
|
|
957
|
+
return { success: true, reason: "global scan", lineUpdated: i };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return { success: false, reason: "not found anywhere in file" };
|
|
961
|
+
}
|
|
962
|
+
// ─────────────────────────────────────────────────────────────
|
|
963
|
+
// UTILITIES
|
|
964
|
+
// ─────────────────────────────────────────────────────────────
|
|
965
|
+
static extractAttrValueFromDom(domContext, oldSelector) {
|
|
966
|
+
const attrMatch = oldSelector.match(/\[([a-zA-Z-]+)=/);
|
|
967
|
+
if (!attrMatch)
|
|
968
|
+
return null;
|
|
969
|
+
const attrName = attrMatch[1];
|
|
970
|
+
const domValueMatch = domContext.match(new RegExp(`${attrName.replace(/-/g, "\\-")}=["']([^"']*)["']`));
|
|
971
|
+
return domValueMatch?.[1] ?? null;
|
|
972
|
+
}
|
|
973
|
+
static resolveFilePath(raw) {
|
|
974
|
+
let clean = raw
|
|
975
|
+
.replace(/^.*?at\s+/, "")
|
|
976
|
+
.replace(/:\d+:\d+.*$/, "")
|
|
977
|
+
.trim();
|
|
978
|
+
if (!path.isAbsolute(clean))
|
|
979
|
+
clean = path.resolve(process.cwd(), clean);
|
|
980
|
+
return fs.existsSync(clean) ? clean : null;
|
|
981
|
+
}
|
|
982
|
+
static updateArgument(caller, action, oldArgument, newArgument) {
|
|
983
|
+
const filePath = SourceUpdater.resolveFilePath(caller.filePath);
|
|
984
|
+
if (!filePath)
|
|
985
|
+
return { success: false, reason: `File not found: ${caller.filePath}` };
|
|
986
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
987
|
+
const lines = raw.split(/\r?\n/);
|
|
988
|
+
const callerLine = caller.line - 1;
|
|
989
|
+
const windowSize = 5;
|
|
990
|
+
const start = Math.max(0, callerLine - windowSize);
|
|
991
|
+
const end = Math.min(lines.length - 1, callerLine + windowSize);
|
|
992
|
+
const escapedAction = action.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
993
|
+
const escapedOldArg = oldArgument.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
994
|
+
for (const q of [`"`, `'`]) {
|
|
995
|
+
const pattern = new RegExp(`(\\.${escapedAction}\\(${q})${escapedOldArg}(${q})`);
|
|
996
|
+
for (let i = callerLine; i >= start; i--) {
|
|
997
|
+
if (pattern.test(lines[i])) {
|
|
998
|
+
const safeNew = sanitizeForInjection(newArgument, q);
|
|
999
|
+
lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
|
|
1000
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
1001
|
+
return { success: true, reason: "argument updated", lineUpdated: i };
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
for (let i = callerLine + 1; i <= end; i++) {
|
|
1005
|
+
if (pattern.test(lines[i])) {
|
|
1006
|
+
const safeNew = sanitizeForInjection(newArgument, q);
|
|
1007
|
+
lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
|
|
1008
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
1009
|
+
return { success: true, reason: "argument updated", lineUpdated: i };
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
console.warn(`[SourceUpdater] ❌ Argument "${oldArgument}" not found near line ${caller.line}`);
|
|
1014
|
+
return { success: false, reason: "argument not found" };
|
|
1015
|
+
}
|
|
1016
|
+
/** Flush buffered advisories to console. Call after Playwright prints its test result. */
|
|
1017
|
+
static flushAdvisories() {
|
|
1018
|
+
astUpdater.flushAdvisories();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
exports.SourceUpdater = SourceUpdater;
|