deflake 1.2.46 → 1.2.48
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/cli.js +171 -4
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -105,6 +105,96 @@ function parseAxTreeSelectors(axTreeContent) {
|
|
|
105
105
|
return selectors;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// --- SELECTOR VALIDATION: Check if AI's proposed selector matches the AX Tree ---
|
|
109
|
+
// TODO [PREMIUM]: Browser-based Component Testing — launch headless browser, navigate to
|
|
110
|
+
// the failing page URL with saved auth state, and run proposed selector against the live DOM.
|
|
111
|
+
// This would catch false positives from AX tree matching and validate dynamic/conditional elements.
|
|
112
|
+
function validateSelectorAgainstAxTree(newLine, axTreeContent) {
|
|
113
|
+
if (!newLine || !axTreeContent) return { valid: true, reason: 'no data to validate' };
|
|
114
|
+
|
|
115
|
+
// Extract the selector pattern from the proposed fix line
|
|
116
|
+
// Patterns: getByRole('role', { name: 'X' }), getByText('X'), getByPlaceholder('X'), getByLabel('X')
|
|
117
|
+
|
|
118
|
+
// 1. getByRole('role', { name: 'X' }) or { name: /regex/ }
|
|
119
|
+
const roleMatch = newLine.match(/getByRole\s*\(\s*['"](\w+)['"]\s*,\s*\{\s*name:\s*(?:['"]([^'"]+)['"]|\/([^/]+)\/)/);
|
|
120
|
+
if (roleMatch) {
|
|
121
|
+
const [, role, exactName, regexName] = roleMatch;
|
|
122
|
+
// Map Playwright roles to AX tree roles
|
|
123
|
+
const axRole = role === 'heading' ? 'heading' : role === 'button' ? 'button' : role === 'link' ? 'link' : role === 'tab' ? 'tab' : role;
|
|
124
|
+
|
|
125
|
+
if (exactName) {
|
|
126
|
+
const found = axTreeContent.includes(`${axRole} "${exactName}"`) || axTreeContent.includes(`${axRole} '${exactName}'`);
|
|
127
|
+
if (!found) {
|
|
128
|
+
// Try partial match for names that might be slightly different
|
|
129
|
+
const partialFound = axTreeContent.toLowerCase().includes(`${axRole}`) && axTreeContent.toLowerCase().includes(exactName.toLowerCase());
|
|
130
|
+
if (!partialFound) {
|
|
131
|
+
return { valid: false, reason: `No ${axRole} with name "${exactName}" found in the page` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if (regexName) {
|
|
135
|
+
try {
|
|
136
|
+
const regex = new RegExp(regexName, 'i');
|
|
137
|
+
// Search AX tree for any matching role+name
|
|
138
|
+
const lines = axTreeContent.split('\n');
|
|
139
|
+
const found = lines.some(line => {
|
|
140
|
+
const m = line.match(new RegExp(`- ${axRole}\\s+["']([^"']+)["']`, 'i'));
|
|
141
|
+
return m && regex.test(m[1]);
|
|
142
|
+
});
|
|
143
|
+
if (!found) {
|
|
144
|
+
return { valid: false, reason: `No ${axRole} matching /${regexName}/ found in the page` };
|
|
145
|
+
}
|
|
146
|
+
} catch (e) { /* regex parse error, skip validation */ }
|
|
147
|
+
}
|
|
148
|
+
return { valid: true, reason: `${axRole} found in AX tree` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. getByText('X')
|
|
152
|
+
const textMatch = newLine.match(/getByText\s*\(\s*['"]([^'"]+)['"]/);
|
|
153
|
+
if (textMatch) {
|
|
154
|
+
const text = textMatch[1];
|
|
155
|
+
const found = axTreeContent.includes(`text: ${text}`) || axTreeContent.includes(`"${text}"`) || axTreeContent.includes(`'${text}'`) || axTreeContent.toLowerCase().includes(text.toLowerCase());
|
|
156
|
+
if (!found) {
|
|
157
|
+
return { valid: false, reason: `No text "${text}" found on the page` };
|
|
158
|
+
}
|
|
159
|
+
return { valid: true, reason: `text "${text}" found` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 3. getByPlaceholder('X')
|
|
163
|
+
const phMatch = newLine.match(/getByPlaceholder\s*\(\s*['"]([^'"]+)['"]/);
|
|
164
|
+
if (phMatch) {
|
|
165
|
+
const ph = phMatch[1];
|
|
166
|
+
const found = axTreeContent.includes(ph) || axTreeContent.toLowerCase().includes(ph.toLowerCase());
|
|
167
|
+
if (!found) {
|
|
168
|
+
return { valid: false, reason: `No placeholder "${ph}" found on the page` };
|
|
169
|
+
}
|
|
170
|
+
return { valid: true, reason: `placeholder "${ph}" found` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4. getByLabel('X')
|
|
174
|
+
const labelMatch = newLine.match(/getByLabel\s*\(\s*['"]([^'"]+)['"]/);
|
|
175
|
+
if (labelMatch) {
|
|
176
|
+
const label = labelMatch[1];
|
|
177
|
+
const found = axTreeContent.includes(label) || axTreeContent.toLowerCase().includes(label.toLowerCase());
|
|
178
|
+
if (!found) {
|
|
179
|
+
return { valid: false, reason: `No label "${label}" found on the page` };
|
|
180
|
+
}
|
|
181
|
+
return { valid: true, reason: `label "${label}" found` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 5. CSS class selectors — REJECT immediately if using .ant-* classes
|
|
185
|
+
if (/\.(ant-|css-dev-only|anticon-)/.test(newLine)) {
|
|
186
|
+
return { valid: false, reason: 'Uses banned CSS class selector (.ant-*/css-dev-only-*)' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 6. locator('.parent') followed by .locator('..') — parent traversal is OK
|
|
190
|
+
if (/locator\s*\(\s*['"]\.\.['"]/.test(newLine)) {
|
|
191
|
+
return { valid: true, reason: 'parent traversal locator (..)' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Default: can't validate, allow through
|
|
195
|
+
return { valid: true, reason: 'selector type not validated (passed through)' };
|
|
196
|
+
}
|
|
197
|
+
|
|
108
198
|
// --- PREMIUM COLORS ---
|
|
109
199
|
const C = {
|
|
110
200
|
RESET: "\x1b[0m",
|
|
@@ -285,6 +375,7 @@ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '', backu
|
|
|
285
375
|
|
|
286
376
|
console.log(`${C.BRIGHT}🔍 Analyzing ${artifacts.length} failure(s)...${C.RESET}\n`);
|
|
287
377
|
let count = 0;
|
|
378
|
+
const fixedFiles = new Set(); // Dedup: track files already fixed to avoid cascading mutations
|
|
288
379
|
for (let i = 0; i < artifacts.length; i++) {
|
|
289
380
|
const art = artifacts[i];
|
|
290
381
|
console.log(`${C.CYAN}━━━ [${i+1}/${artifacts.length}] ${art.name} ━━━${C.RESET}`);
|
|
@@ -314,6 +405,14 @@ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '', backu
|
|
|
314
405
|
// Step 1: Location extraction
|
|
315
406
|
if (loc) {
|
|
316
407
|
console.log(` ${C.GRAY}📍 Location:${C.RESET} ${path.basename(loc.path)}:${loc.line} ${C.GRAY}(from ${locSource})${C.RESET}`);
|
|
408
|
+
|
|
409
|
+
// Dedup: skip if this source file was already fixed by a previous artifact
|
|
410
|
+
const fileKey = loc.path;
|
|
411
|
+
if (fixedFiles.has(fileKey)) {
|
|
412
|
+
console.log(` ${C.GREEN}⏭️ Already fixed by a previous artifact — skipping duplicate${C.RESET}`);
|
|
413
|
+
console.log('');
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
317
416
|
} else {
|
|
318
417
|
console.log(` ${C.YELLOW}📍 Location: Could not extract from any source${C.RESET}`);
|
|
319
418
|
}
|
|
@@ -328,14 +427,81 @@ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '', backu
|
|
|
328
427
|
console.log(` ${C.GRAY}🎯 Available selectors: ${axSelectors.length} elements found in AX Tree${C.RESET}`);
|
|
329
428
|
}
|
|
330
429
|
|
|
331
|
-
// Step 4: API call
|
|
430
|
+
// Step 4: API call with validation-retry loop
|
|
431
|
+
// Validate proposed selectors against AX tree before applying (component testing lite)
|
|
332
432
|
const selectorsContext = axSelectors.length > 0
|
|
333
433
|
? `\n\n=== AVAILABLE SELECTORS (pre-computed from page) ===\nThese selectors are VERIFIED to exist on the page. Pick from this list:\n${axSelectors.map((s, i) => `${i+1}. ${s}`).join('\n')}\n=== END AVAILABLE SELECTORS ===`
|
|
334
434
|
: '';
|
|
335
|
-
const enrichedOutput = (relevantOutput || '') + selectorsContext;
|
|
336
435
|
|
|
337
|
-
|
|
338
|
-
|
|
436
|
+
const MAX_VALIDATION_RETRIES = 3;
|
|
437
|
+
let validatedRes = null;
|
|
438
|
+
let rejectionHistory = '';
|
|
439
|
+
|
|
440
|
+
for (let retryIdx = 0; retryIdx < MAX_VALIDATION_RETRIES; retryIdx++) {
|
|
441
|
+
const retryLabel = retryIdx > 0 ? ` (validation retry ${retryIdx}/${MAX_VALIDATION_RETRIES - 1})` : '';
|
|
442
|
+
const enrichedOutput = (relevantOutput || '') + selectorsContext + rejectionHistory;
|
|
443
|
+
|
|
444
|
+
console.log(` ${C.GRAY}🌐 Calling DeFlake API...${retryLabel}${C.RESET}`);
|
|
445
|
+
const res = await client.heal(null, art.htmlPath, loc, source, argv.fix, enrichedOutput);
|
|
446
|
+
|
|
447
|
+
if (!res || res.status !== 'success') {
|
|
448
|
+
validatedRes = res; // pass through non-success for handling below
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const parsed = JSON.parse(res.fix);
|
|
454
|
+
if (!parsed.patches || parsed.patches.length === 0) {
|
|
455
|
+
validatedRes = res;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Validate each patch's proposed selector against the AX tree
|
|
460
|
+
let allValid = true;
|
|
461
|
+
const rejections = [];
|
|
462
|
+
|
|
463
|
+
for (const p of parsed.patches) {
|
|
464
|
+
if (p.new_line && p.action === 'REPLACE') {
|
|
465
|
+
const validation = validateSelectorAgainstAxTree(p.new_line, content);
|
|
466
|
+
if (!validation.valid) {
|
|
467
|
+
allValid = false;
|
|
468
|
+
rejections.push(`Line ${p.line}: "${p.new_line.trim()}" → REJECTED: ${validation.reason}`);
|
|
469
|
+
console.log(` ${C.YELLOW}🔍 Validation FAILED for line ${p.line}: ${validation.reason}${C.RESET}`);
|
|
470
|
+
} else {
|
|
471
|
+
console.log(` ${C.GREEN}🔍 Validation OK for line ${p.line}: ${validation.reason}${C.RESET}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (allValid) {
|
|
477
|
+
validatedRes = res;
|
|
478
|
+
if (retryIdx > 0) {
|
|
479
|
+
console.log(` ${C.GREEN}✅ All selectors validated after ${retryIdx + 1} attempt(s)${C.RESET}`);
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Build rejection feedback for the next retry
|
|
485
|
+
rejectionHistory = `\n\n=== PREVIOUS FIX REJECTED (attempt ${retryIdx + 1}) ===\n` +
|
|
486
|
+
`Your previous fix was REJECTED because the proposed selectors do NOT exist on the page:\n` +
|
|
487
|
+
rejections.join('\n') + '\n' +
|
|
488
|
+
`Pick DIFFERENT selectors from the AVAILABLE SELECTORS list. Do NOT repeat rejected selectors.\n` +
|
|
489
|
+
`=== END REJECTION ===`;
|
|
490
|
+
|
|
491
|
+
console.log(` ${C.YELLOW}⚠️ Selectors rejected — requesting new fix from AI...${C.RESET}`);
|
|
492
|
+
|
|
493
|
+
} catch (e) {
|
|
494
|
+
validatedRes = res; // parse error, pass through
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!validatedRes && rejectionHistory) {
|
|
500
|
+
console.log(` ${C.RED}❌ Could not find valid selectors after ${MAX_VALIDATION_RETRIES} attempts${C.RESET}`);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const res = validatedRes;
|
|
339
505
|
|
|
340
506
|
if (res && res.status === 'success') {
|
|
341
507
|
console.log(` ${C.GREEN}✅ API Response: SUCCESS${C.RESET}`);
|
|
@@ -363,6 +529,7 @@ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '', backu
|
|
|
363
529
|
console.log(` ${C.BRIGHT}💉 Applying patches...${C.RESET}`);
|
|
364
530
|
if (await applyFix(res, fixLoc, backups)) {
|
|
365
531
|
count++;
|
|
532
|
+
fixedFiles.add(fixLoc.path); // Track: don't fix this file again
|
|
366
533
|
console.log(` ${C.GREEN}${C.BRIGHT}✅ Fix applied to ${path.basename(fixLoc.path)}:${fixLoc.line}${C.RESET}`);
|
|
367
534
|
} else {
|
|
368
535
|
console.log(` ${C.YELLOW}⚠️ Patches could not be applied (see details above)${C.RESET}`);
|