deflake 1.2.1 → 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 +76 -28
- package/client.js +32 -0
- package/lib/mapping.json +17 -0
- package/lib/migrator.js +345 -0
- package/package.json +1 -1
- package/sample-cypress/login.cy.js +9 -0
- package/sample-cypress-pom/login_pom.cy.js +11 -0
- package/sample-cypress-pom/pages/LoginPage.js +12 -0
- package/sample-cypress-ts/login_ts.cy.js +11 -0
- package/sample-cypress-ts/pages/LoginPage.ts +12 -0
- package/sample-playwright/login.spec.spec.ts +15 -0
- package/sample-playwright-ai-v2/login_pom.spec.ts +17 -0
- package/sample-playwright-ai-v2/pages/LoginPage.ts +18 -0
- package/sample-playwright-final/login.spec.ts +15 -0
- package/sample-playwright-pom/login_pom.spec.ts +17 -0
- package/sample-playwright-pom/pages/LoginPage.spec.ts +18 -0
- package/sample-playwright-pom-v2/login_pom.spec.ts +17 -0
- package/sample-playwright-pom-v2/pages/LoginPage.spec.ts +18 -0
- package/sample-playwright-pom-v3/login_pom.spec.ts +17 -0
- package/sample-playwright-pom-v3/pages/LoginPage.spec.ts +18 -0
- package/sample-playwright-pom-v4/login_pom.spec.ts +17 -0
- package/sample-playwright-pom-v4/pages/LoginPage.spec.ts +18 -0
- package/sample-playwright-ts-v2/login_ts.spec.ts +17 -0
- package/sample-playwright-ts-v2/pages/LoginPage.ts +18 -0
- package/sample-playwright-v2/login.spec.ts +11 -0
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
|
|
332
|
-
const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)
|
|
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(/[
|
|
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 =
|
|
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 === '
|
|
744
|
-
const cypressBlocks = (framework === '
|
|
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 === '
|
|
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
|
-
|
|
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);
|
|
@@ -800,9 +843,12 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
800
843
|
const key = `${locId}|${fixCode.trim()}`;
|
|
801
844
|
|
|
802
845
|
if (!groups[key]) {
|
|
803
|
-
groups[key] = { ...res, count: 0 };
|
|
846
|
+
groups[key] = { ...res, count: 0, locations: [] };
|
|
804
847
|
}
|
|
805
848
|
groups[key].count++;
|
|
849
|
+
if (res.location) {
|
|
850
|
+
groups[key].locations.push(res.location);
|
|
851
|
+
}
|
|
806
852
|
}
|
|
807
853
|
|
|
808
854
|
const finalGroups = Object.values(groups);
|
|
@@ -811,9 +857,11 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
811
857
|
console.log(`${C.GRAY}ℹ️ Suggested for ${group.count} similar failures:${C.RESET}`);
|
|
812
858
|
}
|
|
813
859
|
|
|
814
|
-
// APPLY FIX (
|
|
860
|
+
// APPLY FIX (To all instances in the group)
|
|
815
861
|
if (argv.fix) {
|
|
816
|
-
|
|
862
|
+
for (const loc of group.locations) {
|
|
863
|
+
await applySelfHealing({ ...group, location: loc });
|
|
864
|
+
}
|
|
817
865
|
}
|
|
818
866
|
|
|
819
867
|
printDetailedFix(group.fix, group.location, group.source_code, argv.fix);
|
|
@@ -847,10 +895,10 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
847
895
|
}
|
|
848
896
|
|
|
849
897
|
async function main() {
|
|
850
|
-
const command = argv._;
|
|
898
|
+
const command = argv._ || [];
|
|
851
899
|
|
|
852
|
-
// If 'doctor' was called, don't proceed to wrapper logic
|
|
853
|
-
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;
|
|
854
902
|
|
|
855
903
|
console.log(`🚑 DeFlake JS Client (Batch Mode) - ${C.CYAN}v${pkg.version}${C.RESET}`);
|
|
856
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;
|
package/lib/mapping.json
ADDED
|
@@ -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
|
+
}
|
package/lib/migrator.js
ADDED
|
@@ -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
|
@@ -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,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,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
|
+
})
|