deflake 1.2.46 → 1.2.47

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 +161 -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",
@@ -328,14 +418,81 @@ async function analyzeAndFix(artifacts, client, argv, capturedOutput = '', backu
328
418
  console.log(` ${C.GRAY}🎯 Available selectors: ${axSelectors.length} elements found in AX Tree${C.RESET}`);
329
419
  }
330
420
 
331
- // Step 4: API call send error-context, console error block, AND available selectors
421
+ // Step 4: API call with validation-retry loop
422
+ // Validate proposed selectors against AX tree before applying (component testing lite)
332
423
  const selectorsContext = axSelectors.length > 0
333
424
  ? `\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
425
  : '';
335
- const enrichedOutput = (relevantOutput || '') + selectorsContext;
336
426
 
337
- console.log(` ${C.GRAY}🌐 Calling DeFlake API...${C.RESET}`);
338
- const res = await client.heal(null, art.htmlPath, loc, source, argv.fix, enrichedOutput);
427
+ const MAX_VALIDATION_RETRIES = 3;
428
+ let validatedRes = null;
429
+ let rejectionHistory = '';
430
+
431
+ for (let retryIdx = 0; retryIdx < MAX_VALIDATION_RETRIES; retryIdx++) {
432
+ const retryLabel = retryIdx > 0 ? ` (validation retry ${retryIdx}/${MAX_VALIDATION_RETRIES - 1})` : '';
433
+ const enrichedOutput = (relevantOutput || '') + selectorsContext + rejectionHistory;
434
+
435
+ console.log(` ${C.GRAY}🌐 Calling DeFlake API...${retryLabel}${C.RESET}`);
436
+ const res = await client.heal(null, art.htmlPath, loc, source, argv.fix, enrichedOutput);
437
+
438
+ if (!res || res.status !== 'success') {
439
+ validatedRes = res; // pass through non-success for handling below
440
+ break;
441
+ }
442
+
443
+ try {
444
+ const parsed = JSON.parse(res.fix);
445
+ if (!parsed.patches || parsed.patches.length === 0) {
446
+ validatedRes = res;
447
+ break;
448
+ }
449
+
450
+ // Validate each patch's proposed selector against the AX tree
451
+ let allValid = true;
452
+ const rejections = [];
453
+
454
+ for (const p of parsed.patches) {
455
+ if (p.new_line && p.action === 'REPLACE') {
456
+ const validation = validateSelectorAgainstAxTree(p.new_line, content);
457
+ if (!validation.valid) {
458
+ allValid = false;
459
+ rejections.push(`Line ${p.line}: "${p.new_line.trim()}" → REJECTED: ${validation.reason}`);
460
+ console.log(` ${C.YELLOW}🔍 Validation FAILED for line ${p.line}: ${validation.reason}${C.RESET}`);
461
+ } else {
462
+ console.log(` ${C.GREEN}🔍 Validation OK for line ${p.line}: ${validation.reason}${C.RESET}`);
463
+ }
464
+ }
465
+ }
466
+
467
+ if (allValid) {
468
+ validatedRes = res;
469
+ if (retryIdx > 0) {
470
+ console.log(` ${C.GREEN}✅ All selectors validated after ${retryIdx + 1} attempt(s)${C.RESET}`);
471
+ }
472
+ break;
473
+ }
474
+
475
+ // Build rejection feedback for the next retry
476
+ rejectionHistory = `\n\n=== PREVIOUS FIX REJECTED (attempt ${retryIdx + 1}) ===\n` +
477
+ `Your previous fix was REJECTED because the proposed selectors do NOT exist on the page:\n` +
478
+ rejections.join('\n') + '\n' +
479
+ `Pick DIFFERENT selectors from the AVAILABLE SELECTORS list. Do NOT repeat rejected selectors.\n` +
480
+ `=== END REJECTION ===`;
481
+
482
+ console.log(` ${C.YELLOW}⚠️ Selectors rejected — requesting new fix from AI...${C.RESET}`);
483
+
484
+ } catch (e) {
485
+ validatedRes = res; // parse error, pass through
486
+ break;
487
+ }
488
+ }
489
+
490
+ if (!validatedRes && rejectionHistory) {
491
+ console.log(` ${C.RED}❌ Could not find valid selectors after ${MAX_VALIDATION_RETRIES} attempts${C.RESET}`);
492
+ continue;
493
+ }
494
+
495
+ const res = validatedRes;
339
496
 
340
497
  if (res && res.status === 'success') {
341
498
  console.log(` ${C.GREEN}✅ API Response: SUCCESS${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.47",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {