deflake 1.2.2 → 1.2.4

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 CHANGED
@@ -2,6 +2,7 @@
2
2
  const yargs = require('yargs/yargs');
3
3
  const { hideBin } = require('yargs/helpers');
4
4
  const DeFlakeClient = require('./client');
5
+ const Migrator = require('./lib/migrator');
5
6
  const { spawn } = require('child_process');
6
7
  const fs = require('fs');
7
8
  const path = require('path');
@@ -61,9 +62,31 @@ const parser = yargs(hideBin(process.argv))
61
62
  description: 'Diagnose your DeFlake installation',
62
63
  default: false
63
64
  })
65
+ .option('report', {
66
+ type: 'boolean',
67
+ description: 'Automatically open the HTML report after fixing',
68
+ default: true
69
+ })
64
70
  .command('doctor', 'Diagnose your DeFlake installation', {}, (argv) => {
65
71
  runDoctor(argv).then(() => process.exit(0));
66
72
  })
73
+ .command('migrate', 'Migrate Cypress tests to Playwright', {
74
+ from: { type: 'string', default: 'cypress', description: 'Source framework' },
75
+ to: { type: 'string', default: 'playwright', description: 'Target framework' },
76
+ path: { type: 'string', demandOption: true, description: 'Path to Cypress tests' },
77
+ output: { type: 'string', description: 'Path to output Playwright tests' },
78
+ ai: { type: 'boolean', default: false, description: 'Enable AI-powered refinement' }
79
+ }, async (argv) => {
80
+ try {
81
+ const client = new DeFlakeClient(argv['api-url']);
82
+ const migrator = new Migrator({ ...argv, client });
83
+ await migrator.run();
84
+ process.exit(0);
85
+ } catch (err) {
86
+ console.error(`\x1b[31mMigration failed:\x1b[0m ${err.message}`);
87
+ process.exit(1);
88
+ }
89
+ })
67
90
  .version(pkg.version)
68
91
  .help();
69
92
 
@@ -220,6 +243,28 @@ function parseCypressLogs(fullLog) {
220
243
  return specBlocks;
221
244
  }
222
245
 
246
+ function parsePytestLogs(fullLog) {
247
+ const cleanLog = stripAnsi(fullLog);
248
+ const errorBlocks = [];
249
+ const testHeaderRegex = /_{10,}\s+(.*?)\s+_{10,}/g;
250
+ let match;
251
+
252
+ const indices = [];
253
+ while ((match = testHeaderRegex.exec(cleanLog)) !== null) {
254
+ indices.push({ index: match.index, name: match[1] });
255
+ }
256
+
257
+ for (let i = 0; i < indices.length; i++) {
258
+ const start = indices[i].index;
259
+ const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
260
+ errorBlocks.push({
261
+ name: indices[i].name,
262
+ content: cleanLog.slice(start, end)
263
+ });
264
+ }
265
+ return errorBlocks;
266
+ }
267
+
223
268
  async function runHealer(logContent, htmlPath, apiUrl, testName, applyFix = false) {
224
269
  // Check file size limit (Basic check to avoid 429s on huge files)
225
270
  if (htmlPath && fs.existsSync(htmlPath)) {
@@ -321,21 +366,25 @@ function extractFailureLocation(logText) {
321
366
 
322
367
  const projectName = path.basename(process.cwd());
323
368
 
324
- // Updated regex to be more flexible with arrows and spaces, and support .cy files
325
- const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy)\.(?:ts|js)):(\d+):(\d+)/m);
369
+ // Updated regex to be more flexible with arrows and spaces, and support .cy/.py files
370
+ const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy|py)\.(?:ts|js|py)?):(\d+):(\d+)/m) ||
371
+ logText.match(/^(.*?\.py):(\d+):/m); // Pytest direct format
372
+
326
373
  if (testMatch) {
327
374
  loc.specFile = testMatch[1];
328
375
  loc.testLine = testMatch[2];
329
376
  }
330
377
 
331
- // Stack Trace Regex - Modified to handle Cypress URLs and webpack paths
332
- const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
378
+ // Stack Trace Regex - Modified to handle Cypress URLs, webpack paths, and Python "File" entries
379
+ const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?|File "(.+?)", line (\d+)/g;
333
380
  let match;
334
381
  let foundRoot = false;
335
382
 
336
383
  while ((match = stackRegex.exec(logText)) !== null) {
337
- let file = match[1];
338
- const line = match[2];
384
+ let file = match[1] || match[4];
385
+ const line = match[2] || match[5];
386
+
387
+ if (!file) continue;
339
388
 
340
389
  // Clean Cypress/Browser URLs to just the relative path if possible
341
390
  if (file.includes('__cypress/runner')) continue;
@@ -347,16 +396,14 @@ function extractFailureLocation(logText) {
347
396
  }
348
397
 
349
398
  // Strip project name if it's the first segment (common in Cypress/Webpack logs)
350
- // e.g. "cypress-poc/cypress/e2e/api/auth.api.cy.ts" -> "cypress/e2e/api/auth.api.cy.ts"
351
- // Also handle "./" prefix before project name
352
399
  file = file.replace(/^\.\//, '');
353
400
  if (file.startsWith(projectName + '/')) {
354
401
  file = file.substring(projectName.length + 1);
355
402
  }
356
403
  file = file.replace(/^\.\//, '');
357
404
 
358
- if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner')) {
359
- loc.rootFile = file.split(/[/\\]/).pop();
405
+ if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner') && !file.includes('python')) {
406
+ loc.rootFile = file.split(/[/\\\\]/).pop();
360
407
  loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
361
408
  loc.rootLine = line;
362
409
  foundRoot = true;
@@ -368,7 +415,7 @@ function extractFailureLocation(logText) {
368
415
  }
369
416
 
370
417
  // Fallback: If header regex failed but we found a root file that looks like a test
371
- if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.'))) {
418
+ if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.') || loc.rootFile.endsWith('.py'))) {
372
419
  loc.specFile = loc.rootFile;
373
420
  loc.testLine = loc.rootLine;
374
421
  }
@@ -532,7 +579,7 @@ async function runDoctor(argv) {
532
579
  console.log(` ✅ Active: ${C.GREEN}${C.BRIGHT}${capitalized}${C.RESET}`);
533
580
  } else {
534
581
  console.log(` ⚠️ ${C.YELLOW}No supported frameworks detected in the current directory.${C.RESET}`);
535
- console.log(` (Checked for Playwright, Cypress, WebdriverIO files)`);
582
+ console.log(` (Checked for Playwright, Cypress, WebdriverIO, and Selenium Python files)`);
536
583
  }
537
584
 
538
585
  // 2b. Deep Structure Detection
@@ -707,6 +754,55 @@ async function applySelfHealing(result) {
707
754
  }
708
755
  }
709
756
 
757
+ /**
758
+ * Automagically opens the framework's native HTML report.
759
+ */
760
+ function showFrameworkReport() {
761
+ const framework = DeFlakeClient.detectFramework();
762
+ console.log(`\n${C.CYAN}📊 Opening HTML Report for ${framework.toUpperCase()}...${C.RESET}`);
763
+
764
+ let command = '';
765
+ const opener = process.platform === 'win32' ? 'start' : 'open';
766
+
767
+ if (framework === 'playwright') {
768
+ command = 'npx playwright show-report';
769
+ } else if (framework === 'cypress') {
770
+ // Broad search for common Cypress HTML reports
771
+ const candidates = [
772
+ 'cypress/reports/html/index.html',
773
+ 'cypress/reports/index.html',
774
+ 'mochawesome-report/mochawesome.html',
775
+ 'reports/index.html'
776
+ ];
777
+
778
+ const found = candidates.find(p => fs.existsSync(p));
779
+ if (found) {
780
+ command = `${opener} ${found}`;
781
+ } else {
782
+ console.log(`${C.GRAY}ℹ️ Cypress HTML report not found. Verify your reporter configuration (e.g. mochawesome).${C.RESET}`);
783
+ console.log(` ${C.GRAY}Looked in: ${candidates.join(', ')}${C.RESET}`);
784
+ return;
785
+ }
786
+ } else if (framework === 'webdriverio') {
787
+ // WDIO usually uses Allure or spec-reporter
788
+ if (fs.existsSync('allure-results')) {
789
+ console.log(` ${C.GRAY}Allure results detected. Attempting to serve...${C.RESET}`);
790
+ command = 'npx allure serve allure-results';
791
+ } else if (fs.existsSync('reports/html/index.html')) {
792
+ command = `${opener} reports/html/index.html`;
793
+ } else {
794
+ console.log(`${C.GRAY}ℹ️ WebdriverIO HTML report not found. If you use Allure, ensure allure-results folder exists.${C.RESET}`);
795
+ return;
796
+ }
797
+ }
798
+
799
+ if (command) {
800
+ // Use inherit if it's a server (like allure serve) or a long-running process
801
+ const isServer = command.includes('serve') || command.includes('show-report');
802
+ spawn(command, { shell: true, stdio: isServer ? 'inherit' : 'ignore' });
803
+ }
804
+ }
805
+
710
806
  async function analyzeFailures(artifacts, fullLog, client) {
711
807
  if (artifacts.length === 0) {
712
808
  console.log("⚠️ No error artifacts found.");
@@ -733,27 +829,24 @@ async function analyzeFailures(artifacts, fullLog, client) {
733
829
  console.log(` (Processing the first ${limit}...)\n`);
734
830
  }
735
831
 
736
- const framework = artifacts.length > 0 && artifacts[0].htmlPath?.includes('cypress') ? 'Cypress' : 'Playwright';
832
+ const framework = DeFlakeClient.detectFramework();
737
833
  const results = [];
738
834
  const processLimit = Math.min(artifacts.length, limit);
739
835
  const batchArtifacts = artifacts.slice(0, processLimit);
740
836
 
741
837
  console.log(`🔍 Analyzing ${batchArtifacts.length} failure(s)...`);
742
838
 
743
- const playwrightBlocks = (framework === 'Playwright') ? parsePlaywrightLogs(fullLog || "") : [];
744
- const cypressBlocks = (framework === 'Cypress') ? parseCypressLogs(fullLog || "") : [];
839
+ const playwrightBlocks = (framework === 'playwright') ? parsePlaywrightLogs(fullLog || "") : [];
840
+ const cypressBlocks = (framework === 'cypress') ? parseCypressLogs(fullLog || "") : [];
841
+ const pytestBlocks = (framework === 'selenium-python') ? parsePytestLogs(fullLog || "") : [];
745
842
 
746
843
  for (const art of batchArtifacts) {
747
844
  let specificLog = fullLog;
748
845
 
749
- if (framework === 'Cypress' && cypressBlocks.length > 0) {
750
- // Match screenshot path to spec log
751
- // Screenshot typically: cypress/screenshots/folder/file.cy.ts/Test Name (failed).png
846
+ if (framework === 'cypress' && cypressBlocks.length > 0) {
752
847
  const match = cypressBlocks.find(b => art.htmlPath && art.htmlPath.includes(b.spec));
753
- if (match) {
754
- specificLog = match.content;
755
- }
756
- } else if (framework === 'Playwright' && playwrightBlocks.length > 0) {
848
+ if (match) specificLog = match.content;
849
+ } else if (framework === 'playwright' && playwrightBlocks.length > 0) {
757
850
  // Heuristic matching for Playwright error blocks
758
851
  let bestMatch = null;
759
852
  let bestScore = -1;
@@ -768,6 +861,10 @@ async function analyzeFailures(artifacts, fullLog, client) {
768
861
  if (bestMatch && bestScore > 0) {
769
862
  specificLog = bestMatch.content;
770
863
  }
864
+ } else if (framework === 'selenium-python' && pytestBlocks.length > 0) {
865
+ // Match pytest block by test name heuristic
866
+ const match = pytestBlocks.find(b => art.name && b.name.includes(art.name.replace('.png', '')));
867
+ if (match) specificLog = match.content;
771
868
  }
772
869
 
773
870
  const displayName = (art.name || 'Unknown Artifact').substring(0, 40);
@@ -849,13 +946,18 @@ async function analyzeFailures(artifacts, fullLog, client) {
849
946
  } else if (!argv.fix) {
850
947
  console.log(`\n${C.BRIGHT}💡 Tip: Use ${C.CYAN}--fix${C.RESET}${C.BRIGHT} to automatically apply these suggested fixes next time.${C.RESET}`);
851
948
  }
949
+
950
+ // TRIGGER REPORT
951
+ if (results.length > 0 && argv.fix && argv.report) {
952
+ showFrameworkReport();
953
+ }
852
954
  }
853
955
 
854
956
  async function main() {
855
- const command = argv._;
957
+ const command = argv._ || [];
856
958
 
857
- // If 'doctor' was called, don't proceed to wrapper logic
858
- if (command.includes('doctor')) return;
959
+ // If 'doctor' or 'migrate' was called, don't proceed to wrapper logic
960
+ if (command.includes('doctor') || command.includes('migrate')) return;
859
961
 
860
962
  console.log(`🚑 DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
861
963
  const client = new DeFlakeClient(argv.apiUrl);
package/client.js CHANGED
@@ -56,6 +56,7 @@ class DeFlakeClient {
56
56
  if (hasFile(/^cypress\.config\.(js|ts|mjs|cjs)$/) || fs.existsSync('cypress.json') || fs.existsSync('cypress')) return 'cypress';
57
57
  if (hasFile(/^playwright\.config\.(js|ts|mjs|cjs)$/)) return 'playwright';
58
58
  if (hasFile(/^wdio\.conf\.(js|ts|mjs|cjs)$/)) return 'webdriverio';
59
+ if (fs.existsSync('pytest.ini') || fs.existsSync('conftest.py') || fs.existsSync('tox.ini')) return 'selenium-python';
59
60
 
60
61
  // 3. Fallback to package.json dependencies
61
62
  if (fs.existsSync('package.json')) {
@@ -68,6 +69,14 @@ class DeFlakeClient {
68
69
  } catch (e) { }
69
70
  }
70
71
 
72
+ // 4. Fallback to Python requirements
73
+ if (fs.existsSync('requirements.txt')) {
74
+ try {
75
+ const reqs = fs.readFileSync('requirements.txt', 'utf8');
76
+ if (reqs.includes('selenium') || reqs.includes('pytest')) return 'selenium-python';
77
+ } catch (e) { }
78
+ }
79
+
71
80
  return 'generic';
72
81
  }
73
82
 
@@ -147,6 +156,29 @@ class DeFlakeClient {
147
156
  return null; // Return null instead of exiting process
148
157
  }
149
158
  }
159
+
160
+ async migrate(sourceCode, framework = 'Playwright') {
161
+ try {
162
+ const migrateUrl = this.apiUrl.replace(/\/deflake$/, '/migrate');
163
+ const payload = {
164
+ source_code: sourceCode,
165
+ framework: framework
166
+ };
167
+
168
+ const response = await axios.post(migrateUrl, payload, {
169
+ headers: {
170
+ 'Content-Type': 'application/json',
171
+ 'X-API-KEY': this.apiKey,
172
+ 'X-Project-Name': this.projectName
173
+ }
174
+ });
175
+
176
+ return response.data;
177
+ } catch (error) {
178
+ console.error(`❌ Migration Refinement Error: ${error.message}`);
179
+ return { refined_code: sourceCode, status: 'error' };
180
+ }
181
+ }
150
182
  }
151
183
 
152
184
  module.exports = DeFlakeClient;
@@ -0,0 +1,17 @@
1
+ {
2
+ "commands": {
3
+ "visit": "goto",
4
+ "get": "locator",
5
+ "type": "fill",
6
+ "click": "click",
7
+ "contains": "locator",
8
+ "url": "url",
9
+ "wait": "waitForTimeout"
10
+ },
11
+ "assertions": {
12
+ "should": "expect",
13
+ "be.visible": "toBeVisible",
14
+ "have.text": "toHaveText",
15
+ "exist": "toBeVisible"
16
+ }
17
+ }
@@ -0,0 +1,345 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const mapping = require('./mapping.json');
4
+
5
+ // Try to require optional dependencies
6
+ let glob;
7
+ try { glob = require('glob'); } catch (e) { glob = null; }
8
+ let j;
9
+ try { j = require('jscodeshift'); } catch (e) { j = null; }
10
+
11
+ /**
12
+ * Migrator handles the conversion of Cypress tests to Playwright
13
+ */
14
+ class Migrator {
15
+ constructor(options = {}) {
16
+ this.from = options.from || 'cypress';
17
+ this.to = options.to || 'playwright';
18
+ this.inputPath = options.path;
19
+ this.outputPath = options.output || path.join(process.cwd(), 'playwright/tests');
20
+ this.client = options.client;
21
+ this.useAI = options.ai || false;
22
+ }
23
+
24
+ async run() {
25
+ if (!j) {
26
+ throw new Error("jscodeshift is not installed. Please run 'npm install jscodeshift'");
27
+ }
28
+
29
+ console.log(`\n🚀 Starting migration from ${this.from} to ${this.to}...`);
30
+
31
+ // 1. Scan files
32
+ const files = await this.scanFiles();
33
+ console.log(`✔ Found ${files.length} test files`);
34
+
35
+ if (files.length === 0) {
36
+ console.log("⚠️ No files found to migrate.");
37
+ return;
38
+ }
39
+
40
+ if (!fs.existsSync(this.outputPath)) {
41
+ fs.mkdirSync(this.outputPath, { recursive: true });
42
+ }
43
+
44
+ // 2. Transform and Generate
45
+ for (const file of files) {
46
+ const relativePath = path.relative(this.inputPath, file);
47
+ const isTS = file.endsWith('.ts') || file.endsWith('.tsx');
48
+
49
+ let targetFilename = path.basename(file);
50
+ const isTestFile = file.includes('.cy.') || !file.includes('pages/') && !file.includes('support/');
51
+
52
+ if (isTestFile) {
53
+ targetFilename = targetFilename.replace(/\.(cy\.)?(js|ts)$/, '.spec.ts').replace(/\.(jsx|tsx)$/, '.spec.tsx');
54
+ } else {
55
+ targetFilename = targetFilename.replace(/\.js$/, '.ts').replace(/\.jsx$/, '.tsx');
56
+ }
57
+
58
+ const targetPath = path.join(this.outputPath, path.dirname(relativePath), targetFilename);
59
+
60
+ console.log(`⏳ Converting: ${path.basename(file)}...`);
61
+ const source = fs.readFileSync(file, 'utf8');
62
+
63
+ // Step A: Deterministic AST Transformation
64
+ let transformed = this.transform(source, isTS ? 'ts' : 'babylon');
65
+
66
+ // Step B: AI Polishing (if enabled)
67
+ if (this.useAI && this.client) {
68
+ console.log(`🧠 Polishing with AI: ${path.basename(file)}...`);
69
+ const aiResult = await this.client.migrate(transformed, this.to);
70
+ if (aiResult && aiResult.refined_code) {
71
+ transformed = aiResult.refined_code;
72
+ }
73
+ }
74
+
75
+ const targetDir = path.dirname(targetPath);
76
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
77
+
78
+ fs.writeFileSync(targetPath, transformed);
79
+ }
80
+
81
+
82
+ console.log(`\n✨ Migration complete!`);
83
+ console.log(`📂 New tests generated in: ${this.outputPath}`);
84
+ }
85
+
86
+ async scanFiles() {
87
+ if (glob) {
88
+ return glob.sync(`${this.inputPath}/**/*.{js,ts,jsx,tsx}`, {
89
+ ignore: '**/node_modules/**'
90
+ });
91
+ }
92
+
93
+ // Fallback simple recursive scan if glob is not available
94
+ return this.manualScan(this.inputPath);
95
+ }
96
+
97
+ manualScan(dir) {
98
+ let results = [];
99
+ const list = fs.readdirSync(dir);
100
+ list.forEach(file => {
101
+ const fullPath = path.join(dir, file);
102
+ const stat = fs.statSync(fullPath);
103
+ if (stat && stat.isDirectory()) {
104
+ if (!file.includes('node_modules')) {
105
+ results = results.concat(this.manualScan(fullPath));
106
+ }
107
+ } else {
108
+ if (file.endsWith('.js') || file.endsWith('.ts')) {
109
+ results.push(fullPath);
110
+ }
111
+ }
112
+ });
113
+ return results;
114
+ }
115
+
116
+ transform(source, parser = 'babylon') {
117
+ if (!j) return source;
118
+ // Use withParser for specific parsers like 'ts'
119
+ const root = parser === 'ts' ? j.withParser('ts')(source) : j(source);
120
+
121
+ // 1. Detect if we are in a Page Object (Class)
122
+ const classes = root.find(j.ClassDeclaration);
123
+
124
+ // 2. Transform Classes for Playwright
125
+ classes.forEach(path => {
126
+ const body = path.value.body.body;
127
+ const hasConstructor = body.some(node => node.type === 'MethodDefinition' && node.kind === 'constructor');
128
+
129
+ if (!hasConstructor) {
130
+ // Inject constructor(page: Page) { this.page = page; }
131
+ const constructor = j.methodDefinition(
132
+ 'constructor',
133
+ j.identifier('constructor'),
134
+ j.functionExpression(
135
+ null,
136
+ [j.identifier('page')],
137
+ j.blockStatement([
138
+ j.expressionStatement(
139
+ j.assignmentExpression(
140
+ '=',
141
+ j.memberExpression(j.thisExpression(), j.identifier('page')),
142
+ j.identifier('page')
143
+ )
144
+ )
145
+ ])
146
+ )
147
+ );
148
+ constructor.kind = 'constructor';
149
+ body.unshift(constructor);
150
+ }
151
+ });
152
+
153
+ // 3. Command Mapping (Context Aware)
154
+ // If inside a POM class, use 'this.page', otherwise use 'page'
155
+ const getContext = (p) => {
156
+ let parent = p.parentPath;
157
+ while (parent) {
158
+ const node = parent.value;
159
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassMethod' || node.type === 'MethodDefinition') {
160
+ return j.memberExpression(j.thisExpression(), j.identifier('page'));
161
+ }
162
+ parent = parent.parentPath;
163
+ }
164
+ return j.identifier('page');
165
+ };
166
+
167
+
168
+ // Replace describe -> test.describe, it -> test
169
+ root.find(j.CallExpression, { callee: { name: 'describe' } })
170
+ .forEach(p => {
171
+ p.get('callee').replace(j.memberExpression(j.identifier('test'), j.identifier('describe')));
172
+ });
173
+
174
+ root.find(j.CallExpression, { callee: { name: 'it' } })
175
+ .forEach(p => {
176
+ p.get('callee').replace(j.identifier('test'));
177
+ });
178
+
179
+ // Replace cy.get/contains with ctx.locator
180
+ root.find(j.CallExpression, {
181
+ callee: {
182
+ object: { name: 'cy' },
183
+ property: { name: 'get' }
184
+ }
185
+ }).replaceWith(p => {
186
+ return j.callExpression(
187
+ j.memberExpression(getContext(p), j.identifier('locator')),
188
+ p.value.arguments
189
+ );
190
+ });
191
+
192
+ root.find(j.CallExpression, {
193
+ callee: {
194
+ object: { name: 'cy' },
195
+ property: { name: 'contains' }
196
+ }
197
+ }).replaceWith(p => {
198
+ return j.callExpression(
199
+ j.memberExpression(getContext(p), j.identifier('locator')),
200
+ [j.templateLiteral([j.templateElement({ raw: 'text=', cooked: 'text=' }, false)], [p.value.arguments[0]])]
201
+ );
202
+ });
203
+
204
+ root.find(j.CallExpression, {
205
+ callee: {
206
+ object: { name: 'cy' },
207
+ property: { name: 'visit' }
208
+ }
209
+ }).replaceWith(p => {
210
+ return j.awaitExpression(
211
+ j.callExpression(
212
+ j.memberExpression(getContext(p), j.identifier('goto')),
213
+ p.value.arguments
214
+ )
215
+ );
216
+ });
217
+
218
+ // Wrap it/test callbacks with async ({ page }) => ...
219
+ root.find(j.CallExpression, { callee: { name: 'test' } })
220
+ .forEach(p => {
221
+ const callback = p.value.arguments[1];
222
+ if (callback && (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')) {
223
+ callback.async = true;
224
+ if (callback.params.length === 0) {
225
+ callback.params = [j.objectPattern([
226
+ j.property.from({
227
+ kind: 'init',
228
+ key: j.identifier('page'),
229
+ value: j.identifier('page'),
230
+ shorthand: true
231
+ })
232
+ ])];
233
+ }
234
+ }
235
+ });
236
+
237
+ // Handle chained commands (click, type, etc)
238
+ Object.entries(mapping.commands).forEach(([cyCmd, pwCmd]) => {
239
+ if (['get', 'contains', 'visit'].includes(cyCmd)) return;
240
+
241
+ root.find(j.CallExpression, {
242
+ callee: { property: { name: cyCmd } }
243
+ }).replaceWith(p => {
244
+ const call = j.callExpression(
245
+ j.memberExpression(p.value.callee.object, j.identifier(pwCmd)),
246
+ p.value.arguments
247
+ );
248
+ // Wrap with await
249
+ return j.awaitExpression(call);
250
+ });
251
+ });
252
+
253
+ // 5. Ensure parent methods are async if they contain await
254
+ root.find(j.AwaitExpression).forEach(p => {
255
+ let parent = p.parentPath;
256
+ while (parent) {
257
+ const node = parent.value;
258
+ if (node.type === 'FunctionExpression' ||
259
+ node.type === 'ArrowFunctionExpression' ||
260
+ node.type === 'ClassMethod' ||
261
+ node.type === 'MethodDefinition' ||
262
+ node.type === 'ObjectMethod') {
263
+
264
+ if (node.type === 'MethodDefinition' || node.type === 'Property') {
265
+ if (node.value && (node.value.type === 'FunctionExpression' || node.value.type === 'ArrowFunctionExpression')) {
266
+ node.value.async = true;
267
+ }
268
+ } else {
269
+ node.async = true;
270
+ }
271
+ break;
272
+ }
273
+ parent = parent.parentPath;
274
+ }
275
+ });
276
+
277
+ // Heuristic: If we are calling a method on an object that looks like a POM, await it
278
+ root.find(j.CallExpression).forEach(p => {
279
+ if (p.value.callee.type === 'MemberExpression' &&
280
+ p.value.callee.object.type === 'Identifier' &&
281
+ p.parentPath.value.type === 'ExpressionStatement') {
282
+
283
+ const objName = p.value.callee.object.name;
284
+ if (objName.toLowerCase().includes('page') || objName.toLowerCase().includes('login') || objName.toLowerCase().includes('pom')) {
285
+ j(p).replaceWith(j.awaitExpression(p.value));
286
+ }
287
+ }
288
+ });
289
+
290
+ // 6. Simple TypeScript Type Injections (Basic)
291
+ if (parser === 'ts') {
292
+ classes.forEach(path => {
293
+ const body = path.value.body.body;
294
+ body.forEach(member => {
295
+ if (member.type === 'MethodDefinition' && member.kind === 'constructor') {
296
+ const param = member.value.params[0];
297
+ if (param && param.name === 'page' && !param.typeAnnotation) {
298
+ param.typeAnnotation = j.tsTypeAnnotation(
299
+ j.tsTypeReference(j.identifier('Page'))
300
+ );
301
+ }
302
+ }
303
+ });
304
+ });
305
+ }
306
+
307
+ // 7. Handle assertions
308
+ root.find(j.CallExpression, {
309
+ callee: { property: { name: 'should' } }
310
+ }).replaceWith(p => {
311
+ const subject = p.value.callee.object;
312
+ const assertion = p.value.arguments[0].value;
313
+ const pwMatcher = mapping.assertions[assertion] || 'toBeVisible';
314
+
315
+ // Clean subject: if it's an await expression, get the inner call
316
+ const cleanSubject = subject.type === 'AwaitExpression' ? subject.argument : subject;
317
+
318
+ const call = j.callExpression(
319
+ j.memberExpression(
320
+ j.callExpression(j.identifier('expect'), [cleanSubject]),
321
+ j.identifier(pwMatcher)
322
+ ),
323
+ p.value.arguments.slice(1)
324
+ );
325
+
326
+ return j.awaitExpression(call);
327
+ });
328
+
329
+ const result = root.toSource();
330
+ // Add imports
331
+ if (!result.includes('@playwright/test')) {
332
+ const imports = parser === 'ts' ? '{ test, expect, Page }' : '{ test, expect }';
333
+ return `import ${imports} from '@playwright/test';\n\n${result}`;
334
+ } else if (parser === 'ts' && result.includes('{ test, expect }') && !result.includes('Page')) {
335
+ return result.replace('{ test, expect }', '{ test, expect, Page }');
336
+ }
337
+
338
+ return result;
339
+
340
+ }
341
+ }
342
+
343
+ module.exports = Migrator;
344
+
345
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deflake",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "AI-powered self-healing tool for Playwright, Cypress, and WebdriverIO tests.",
5
5
  "main": "client.js",
6
6
  "bin": {
@@ -0,0 +1,9 @@
1
+ describe('Login', () => {
2
+ it('should login', () => {
3
+ cy.visit('/login')
4
+ cy.get('#email').type('test@test.com')
5
+ cy.get('#password').type('123456')
6
+ cy.get('button').click()
7
+ cy.contains('Dashboard').should('be.visible')
8
+ })
9
+ })
@@ -0,0 +1,11 @@
1
+ const LoginPage = require('../pages/LoginPage')
2
+
3
+ describe('Login POM', () => {
4
+ const loginPage = new LoginPage()
5
+
6
+ it('should login with POM', () => {
7
+ loginPage.visit()
8
+ loginPage.login('test@test.com', '123456')
9
+ cy.contains('Dashboard').should('be.visible')
10
+ })
11
+ })
@@ -0,0 +1,12 @@
1
+ class LoginPage {
2
+ visit() {
3
+ cy.visit('/login')
4
+ }
5
+
6
+ login(email, password) {
7
+ cy.get('#email').type(email)
8
+ cy.get('#password').type(password)
9
+ cy.get('button').click()
10
+ }
11
+ }
12
+ module.exports = LoginPage;
@@ -0,0 +1,11 @@
1
+ import LoginPage from './pages/LoginPage'
2
+
3
+ describe('Login TS', () => {
4
+ const loginPage = new LoginPage()
5
+
6
+ it('should login with TS', () => {
7
+ loginPage.visit()
8
+ loginPage.login('test@test.com', '123456')
9
+ cy.contains('Dashboard').should('be.visible')
10
+ })
11
+ })
@@ -0,0 +1,12 @@
1
+ class LoginPage {
2
+ visit() {
3
+ cy.visit('/login')
4
+ }
5
+
6
+ login(email: string, password: string) {
7
+ cy.get('#email').type(email)
8
+ cy.get('#password').type(password)
9
+ cy.get('button').click()
10
+ }
11
+ }
12
+ export default LoginPage;
@@ -0,0 +1,15 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('Login', () => {
4
+ test('should login', async (
5
+ {
6
+ page
7
+ }
8
+ ) => {
9
+ await page.goto('/login')
10
+ (await page.locator('#email')).type('test@test.com')
11
+ (await page.locator('#password')).type('123456')
12
+ (await page.locator('button')).click()
13
+ await expect(page.locator('Dashboard')).toBeVisible()
14
+ })
15
+ })
@@ -0,0 +1,17 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ const LoginPage = require('../pages/LoginPage')
4
+
5
+ test.describe('Login POM', () => {
6
+ const loginPage = new LoginPage()
7
+
8
+ test('should login with POM', async (
9
+ {
10
+ page
11
+ }
12
+ ) => {
13
+ await loginPage.visit()
14
+ await loginPage.login('test@test.com', '123456')
15
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
16
+ })
17
+ })
@@ -0,0 +1,18 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ class LoginPage {
4
+ constructor(page) {
5
+ this.page = page;
6
+ }
7
+
8
+ async visit() {
9
+ await this.page.goto('/login')
10
+ }
11
+
12
+ async login(email, password) {
13
+ await this.page.locator('#email').fill(email)
14
+ await this.page.locator('#password').fill(password)
15
+ await this.page.locator('button').click()
16
+ }
17
+ }
18
+ module.exports = LoginPage;
@@ -0,0 +1,15 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('Login', () => {
4
+ test('should login', async (
5
+ {
6
+ page
7
+ }
8
+ ) => {
9
+ await page.goto('/login')
10
+ await page.locator('#email').fill('test@test.com')
11
+ await page.locator('#password').fill('123456')
12
+ await page.locator('button').click()
13
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
14
+ })
15
+ })
@@ -0,0 +1,17 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ const LoginPage = require('../pages/LoginPage')
4
+
5
+ test.describe('Login POM', () => {
6
+ const loginPage = new LoginPage()
7
+
8
+ test('should login with POM', async (
9
+ {
10
+ page
11
+ }
12
+ ) => {
13
+ loginPage.visit()
14
+ loginPage.login('test@test.com', '123456')
15
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
16
+ })
17
+ })
@@ -0,0 +1,18 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ class LoginPage {
4
+ constructor(page) {
5
+ this.page = page;
6
+ }
7
+
8
+ visit() {
9
+ await this.page.goto('/login')
10
+ }
11
+
12
+ login(email, password) {
13
+ await this.page.locator('#email').fill(email)
14
+ await this.page.locator('#password').fill(password)
15
+ await this.page.locator('button').click()
16
+ }
17
+ }
18
+ module.exports = LoginPage;
@@ -0,0 +1,17 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ const LoginPage = require('../pages/LoginPage')
4
+
5
+ test.describe('Login POM', () => {
6
+ const loginPage = new LoginPage()
7
+
8
+ test('should login with POM', async (
9
+ {
10
+ page
11
+ }
12
+ ) => {
13
+ await loginPage.visit()
14
+ await loginPage.login('test@test.com', '123456')
15
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
16
+ })
17
+ })
@@ -0,0 +1,18 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ class LoginPage {
4
+ constructor(page) {
5
+ this.page = page;
6
+ }
7
+
8
+ async visit() {
9
+ await this.page.goto('/login')
10
+ }
11
+
12
+ login(email, password) {
13
+ await this.page.locator('#email').fill(email)
14
+ await this.page.locator('#password').fill(password)
15
+ await this.page.locator('button').click()
16
+ }
17
+ }
18
+ module.exports = LoginPage;
@@ -0,0 +1,17 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ const LoginPage = require('../pages/LoginPage')
4
+
5
+ test.describe('Login POM', () => {
6
+ const loginPage = new LoginPage()
7
+
8
+ test('should login with POM', async (
9
+ {
10
+ page
11
+ }
12
+ ) => {
13
+ await loginPage.visit()
14
+ await loginPage.login('test@test.com', '123456')
15
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
16
+ })
17
+ })
@@ -0,0 +1,18 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ class LoginPage {
4
+ constructor(page) {
5
+ this.page = page;
6
+ }
7
+
8
+ async visit() {
9
+ await this.page.goto('/login')
10
+ }
11
+
12
+ login(email, password) {
13
+ await this.page.locator('#email').fill(email)
14
+ await this.page.locator('#password').fill(password)
15
+ await this.page.locator('button').click()
16
+ }
17
+ }
18
+ module.exports = LoginPage;
@@ -0,0 +1,17 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ const LoginPage = require('../pages/LoginPage')
4
+
5
+ test.describe('Login POM', () => {
6
+ const loginPage = new LoginPage()
7
+
8
+ test('should login with POM', async (
9
+ {
10
+ page
11
+ }
12
+ ) => {
13
+ await loginPage.visit()
14
+ await loginPage.login('test@test.com', '123456')
15
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
16
+ })
17
+ })
@@ -0,0 +1,18 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ class LoginPage {
4
+ constructor(page) {
5
+ this.page = page;
6
+ }
7
+
8
+ async visit() {
9
+ await this.page.goto('/login')
10
+ }
11
+
12
+ async login(email, password) {
13
+ await this.page.locator('#email').fill(email)
14
+ await this.page.locator('#password').fill(password)
15
+ await this.page.locator('button').click()
16
+ }
17
+ }
18
+ module.exports = LoginPage;
@@ -0,0 +1,17 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ import LoginPage from './pages/LoginPage'
4
+
5
+ test.describe('Login TS', () => {
6
+ const loginPage = new LoginPage()
7
+
8
+ test('should login with TS', async (
9
+ {
10
+ page
11
+ }
12
+ ) => {
13
+ await loginPage.visit()
14
+ await loginPage.login('test@test.com', '123456')
15
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
16
+ })
17
+ })
@@ -0,0 +1,18 @@
1
+ import { test, expect, Page } from '@playwright/test';
2
+
3
+ class LoginPage {
4
+ constructor(page: Page) {
5
+ this.page = page;
6
+ }
7
+
8
+ async visit() {
9
+ await this.page.goto('/login')
10
+ }
11
+
12
+ async login(email: string, password: string) {
13
+ await this.page.locator('#email').fill(email)
14
+ await this.page.locator('#password').fill(password)
15
+ await this.page.locator('button').click()
16
+ }
17
+ }
18
+ export default LoginPage;
@@ -0,0 +1,11 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ describe('Login', () => {
4
+ it('should login', () => {
5
+ await page.goto('/login')
6
+ await page.locator('#email').fill('test@test.com')
7
+ await page.locator('#password').fill('123456')
8
+ await page.locator('button').click()
9
+ await expect(page.locator(`text=${'Dashboard'}`)).toBeVisible()
10
+ })
11
+ })