deflake 1.2.2 → 1.2.3

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');
@@ -64,6 +65,23 @@ const parser = yargs(hideBin(process.argv))
64
65
  .command('doctor', 'Diagnose your DeFlake installation', {}, (argv) => {
65
66
  runDoctor(argv).then(() => process.exit(0));
66
67
  })
68
+ .command('migrate', 'Migrate Cypress tests to Playwright', {
69
+ from: { type: 'string', default: 'cypress', description: 'Source framework' },
70
+ to: { type: 'string', default: 'playwright', description: 'Target framework' },
71
+ path: { type: 'string', demandOption: true, description: 'Path to Cypress tests' },
72
+ output: { type: 'string', description: 'Path to output Playwright tests' },
73
+ ai: { type: 'boolean', default: false, description: 'Enable AI-powered refinement' }
74
+ }, async (argv) => {
75
+ try {
76
+ const client = new DeFlakeClient(argv['api-url']);
77
+ const migrator = new Migrator({ ...argv, client });
78
+ await migrator.run();
79
+ process.exit(0);
80
+ } catch (err) {
81
+ console.error(`\x1b[31mMigration failed:\x1b[0m ${err.message}`);
82
+ process.exit(1);
83
+ }
84
+ })
67
85
  .version(pkg.version)
68
86
  .help();
69
87
 
@@ -220,6 +238,28 @@ function parseCypressLogs(fullLog) {
220
238
  return specBlocks;
221
239
  }
222
240
 
241
+ function parsePytestLogs(fullLog) {
242
+ const cleanLog = stripAnsi(fullLog);
243
+ const errorBlocks = [];
244
+ const testHeaderRegex = /_{10,}\s+(.*?)\s+_{10,}/g;
245
+ let match;
246
+
247
+ const indices = [];
248
+ while ((match = testHeaderRegex.exec(cleanLog)) !== null) {
249
+ indices.push({ index: match.index, name: match[1] });
250
+ }
251
+
252
+ for (let i = 0; i < indices.length; i++) {
253
+ const start = indices[i].index;
254
+ const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
255
+ errorBlocks.push({
256
+ name: indices[i].name,
257
+ content: cleanLog.slice(start, end)
258
+ });
259
+ }
260
+ return errorBlocks;
261
+ }
262
+
223
263
  async function runHealer(logContent, htmlPath, apiUrl, testName, applyFix = false) {
224
264
  // Check file size limit (Basic check to avoid 429s on huge files)
225
265
  if (htmlPath && fs.existsSync(htmlPath)) {
@@ -321,21 +361,25 @@ function extractFailureLocation(logText) {
321
361
 
322
362
  const projectName = path.basename(process.cwd());
323
363
 
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);
364
+ // Updated regex to be more flexible with arrows and spaces, and support .cy/.py files
365
+ const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy|py)\.(?:ts|js|py)?):(\d+):(\d+)/m) ||
366
+ logText.match(/^(.*?\.py):(\d+):/m); // Pytest direct format
367
+
326
368
  if (testMatch) {
327
369
  loc.specFile = testMatch[1];
328
370
  loc.testLine = testMatch[2];
329
371
  }
330
372
 
331
- // Stack Trace Regex - Modified to handle Cypress URLs and webpack paths
332
- const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
373
+ // Stack Trace Regex - Modified to handle Cypress URLs, webpack paths, and Python "File" entries
374
+ const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?|File "(.+?)", line (\d+)/g;
333
375
  let match;
334
376
  let foundRoot = false;
335
377
 
336
378
  while ((match = stackRegex.exec(logText)) !== null) {
337
- let file = match[1];
338
- const line = match[2];
379
+ let file = match[1] || match[4];
380
+ const line = match[2] || match[5];
381
+
382
+ if (!file) continue;
339
383
 
340
384
  // Clean Cypress/Browser URLs to just the relative path if possible
341
385
  if (file.includes('__cypress/runner')) continue;
@@ -347,16 +391,14 @@ function extractFailureLocation(logText) {
347
391
  }
348
392
 
349
393
  // 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
394
  file = file.replace(/^\.\//, '');
353
395
  if (file.startsWith(projectName + '/')) {
354
396
  file = file.substring(projectName.length + 1);
355
397
  }
356
398
  file = file.replace(/^\.\//, '');
357
399
 
358
- if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner')) {
359
- loc.rootFile = file.split(/[/\\]/).pop();
400
+ if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner') && !file.includes('python')) {
401
+ loc.rootFile = file.split(/[/\\\\]/).pop();
360
402
  loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
361
403
  loc.rootLine = line;
362
404
  foundRoot = true;
@@ -368,7 +410,7 @@ function extractFailureLocation(logText) {
368
410
  }
369
411
 
370
412
  // 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.'))) {
413
+ if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.') || loc.rootFile.endsWith('.py'))) {
372
414
  loc.specFile = loc.rootFile;
373
415
  loc.testLine = loc.rootLine;
374
416
  }
@@ -532,7 +574,7 @@ async function runDoctor(argv) {
532
574
  console.log(` ✅ Active: ${C.GREEN}${C.BRIGHT}${capitalized}${C.RESET}`);
533
575
  } else {
534
576
  console.log(` ⚠️ ${C.YELLOW}No supported frameworks detected in the current directory.${C.RESET}`);
535
- console.log(` (Checked for Playwright, Cypress, WebdriverIO files)`);
577
+ console.log(` (Checked for Playwright, Cypress, WebdriverIO, and Selenium Python files)`);
536
578
  }
537
579
 
538
580
  // 2b. Deep Structure Detection
@@ -733,27 +775,24 @@ async function analyzeFailures(artifacts, fullLog, client) {
733
775
  console.log(` (Processing the first ${limit}...)\n`);
734
776
  }
735
777
 
736
- const framework = artifacts.length > 0 && artifacts[0].htmlPath?.includes('cypress') ? 'Cypress' : 'Playwright';
778
+ const framework = DeFlakeClient.detectFramework();
737
779
  const results = [];
738
780
  const processLimit = Math.min(artifacts.length, limit);
739
781
  const batchArtifacts = artifacts.slice(0, processLimit);
740
782
 
741
783
  console.log(`🔍 Analyzing ${batchArtifacts.length} failure(s)...`);
742
784
 
743
- const playwrightBlocks = (framework === 'Playwright') ? parsePlaywrightLogs(fullLog || "") : [];
744
- const cypressBlocks = (framework === 'Cypress') ? parseCypressLogs(fullLog || "") : [];
785
+ const playwrightBlocks = (framework === 'playwright') ? parsePlaywrightLogs(fullLog || "") : [];
786
+ const cypressBlocks = (framework === 'cypress') ? parseCypressLogs(fullLog || "") : [];
787
+ const pytestBlocks = (framework === 'selenium-python') ? parsePytestLogs(fullLog || "") : [];
745
788
 
746
789
  for (const art of batchArtifacts) {
747
790
  let specificLog = fullLog;
748
791
 
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
792
+ if (framework === 'cypress' && cypressBlocks.length > 0) {
752
793
  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) {
794
+ if (match) specificLog = match.content;
795
+ } else if (framework === 'playwright' && playwrightBlocks.length > 0) {
757
796
  // Heuristic matching for Playwright error blocks
758
797
  let bestMatch = null;
759
798
  let bestScore = -1;
@@ -768,6 +807,10 @@ async function analyzeFailures(artifacts, fullLog, client) {
768
807
  if (bestMatch && bestScore > 0) {
769
808
  specificLog = bestMatch.content;
770
809
  }
810
+ } else if (framework === 'selenium-python' && pytestBlocks.length > 0) {
811
+ // Match pytest block by test name heuristic
812
+ const match = pytestBlocks.find(b => art.name && b.name.includes(art.name.replace('.png', '')));
813
+ if (match) specificLog = match.content;
771
814
  }
772
815
 
773
816
  const displayName = (art.name || 'Unknown Artifact').substring(0, 40);
@@ -852,10 +895,10 @@ async function analyzeFailures(artifacts, fullLog, client) {
852
895
  }
853
896
 
854
897
  async function main() {
855
- const command = argv._;
898
+ const command = argv._ || [];
856
899
 
857
- // If 'doctor' was called, don't proceed to wrapper logic
858
- if (command.includes('doctor')) return;
900
+ // If 'doctor' or 'migrate' was called, don't proceed to wrapper logic
901
+ if (command.includes('doctor') || command.includes('migrate')) return;
859
902
 
860
903
  console.log(`🚑 DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
861
904
  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.3",
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
+ })