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.
Files changed (2) hide show
  1. package/cli.js +171 -4
  2. 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 send error-context, console error block, AND available selectors
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
- console.log(` ${C.GRAY}🌐 Calling DeFlake API...${C.RESET}`);
338
- const res = await client.heal(null, art.htmlPath, loc, source, argv.fix, enrichedOutput);
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {