deflake 1.0.8 โ 1.0.12
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/README.md +27 -4
- package/cli.js +120 -63
- package/client.js +36 -9
- package/package.json +1 -1
- package/test_location_parser.js +132 -0
package/README.md
CHANGED
|
@@ -12,24 +12,47 @@ npm install deflake --save-dev
|
|
|
12
12
|
## ๐ง Usage
|
|
13
13
|
|
|
14
14
|
### Wrapper Mode (Zero Config)
|
|
15
|
-
Simply prepend `npx deflake` to your test run command
|
|
15
|
+
Simply prepend `npx deflake` to your test run command. DeFlake will automatically wrap the execution and analyze failures.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# Playwright
|
|
19
|
-
export DEFLAKE_API_KEY="your-api-key"
|
|
20
19
|
npx deflake npx playwright test
|
|
21
20
|
|
|
22
21
|
# Cypress
|
|
23
22
|
npx deflake npx cypress run
|
|
24
23
|
```
|
|
25
24
|
|
|
25
|
+
### ๐ Diagnostics (Doctor Mode)
|
|
26
|
+
Validate your environment, API key, and quota without running tests:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx deflake doctor
|
|
30
|
+
```
|
|
31
|
+
|
|
26
32
|
### Manual Mode
|
|
27
|
-
Analyze an existing error log and HTML snapshot:
|
|
33
|
+
Analyze an existing error log and HTML/Image snapshot:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx deflake --log error.log --html cypress/screenshots/my-test-failure.png
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ๐ฒ Cypress Integration Tips
|
|
40
|
+
|
|
41
|
+
For the best experience with Cypress, we recommend adding a standardized script to your `package.json`:
|
|
28
42
|
|
|
43
|
+
```json
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test:deflake": "deflake npx cypress run"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then run it using:
|
|
29
50
|
```bash
|
|
30
|
-
|
|
51
|
+
npm run test:deflake
|
|
31
52
|
```
|
|
32
53
|
|
|
54
|
+
DeFlake automatically scans `cypress/screenshots` to find failure artifacts (images) for analysis.
|
|
55
|
+
|
|
33
56
|
## ๐ Support & Documentation
|
|
34
57
|
|
|
35
58
|
For issues, feature requests, or to get your API key, please visit our [Official Portal](https://deflake-api.up.railway.app).
|
package/cli.js
CHANGED
|
@@ -125,6 +125,22 @@ function detectAllArtifacts(providedLog, providedHtml) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// 2. Scan cypress/screenshots (Cypress default)
|
|
129
|
+
if (fs.existsSync('cypress/screenshots')) {
|
|
130
|
+
const screenshotFiles = fs.readdirSync('cypress/screenshots', { recursive: true })
|
|
131
|
+
.filter(f => f.endsWith('.png'))
|
|
132
|
+
.map(f => path.resolve('cypress/screenshots', f));
|
|
133
|
+
|
|
134
|
+
for (const screenshot of screenshotFiles) {
|
|
135
|
+
detected.push({
|
|
136
|
+
logPath: providedLog,
|
|
137
|
+
htmlPath: screenshot,
|
|
138
|
+
id: path.basename(screenshot),
|
|
139
|
+
name: path.basename(screenshot)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
128
144
|
// Fallback: If no granular results, check global report
|
|
129
145
|
if (detected.length === 0) {
|
|
130
146
|
if (fs.existsSync('playwright-report/index.html')) {
|
|
@@ -272,33 +288,56 @@ function extractFailureLocation(logText) {
|
|
|
272
288
|
stepLine: null
|
|
273
289
|
};
|
|
274
290
|
|
|
275
|
-
|
|
276
|
-
|
|
291
|
+
const projectName = path.basename(process.cwd());
|
|
292
|
+
|
|
293
|
+
// Updated regex to be more flexible with arrows and spaces, and support .cy files
|
|
294
|
+
const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy)\.(?:ts|js)):(\d+):(\d+)/m);
|
|
277
295
|
if (testMatch) {
|
|
278
296
|
loc.specFile = testMatch[1];
|
|
279
297
|
loc.testLine = testMatch[2];
|
|
280
298
|
}
|
|
281
299
|
|
|
282
|
-
|
|
300
|
+
// Stack Trace Regex - Modified to handle Cypress URLs and webpack paths
|
|
301
|
+
const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
|
|
283
302
|
let match;
|
|
284
303
|
let foundRoot = false;
|
|
285
304
|
|
|
286
305
|
while ((match = stackRegex.exec(logText)) !== null) {
|
|
287
|
-
|
|
306
|
+
let file = match[1];
|
|
288
307
|
const line = match[2];
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
|
|
309
|
+
// Clean Cypress/Browser URLs to just the relative path if possible
|
|
310
|
+
if (file.includes('__cypress/runner')) continue;
|
|
311
|
+
|
|
312
|
+
if (file.includes('webpack:///')) {
|
|
313
|
+
file = file.split('webpack:///')[1];
|
|
314
|
+
} else if (file.includes('webpack://')) {
|
|
315
|
+
file = file.split('webpack://')[1];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Strip project name if it's the first segment (common in Cypress/Webpack logs)
|
|
319
|
+
// e.g. "cypress-poc/cypress/e2e/api/auth.api.cy.ts" -> "cypress/e2e/api/auth.api.cy.ts"
|
|
320
|
+
// Also handle "./" prefix before project name
|
|
321
|
+
file = file.replace(/^\.\//, '');
|
|
322
|
+
if (file.startsWith(projectName + '/')) {
|
|
323
|
+
file = file.substring(projectName.length + 1);
|
|
324
|
+
}
|
|
325
|
+
file = file.replace(/^\.\//, '');
|
|
326
|
+
|
|
327
|
+
if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner')) {
|
|
328
|
+
loc.rootFile = file.split(/[/\\]/).pop();
|
|
329
|
+
loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
|
|
292
330
|
loc.rootLine = line;
|
|
293
331
|
foundRoot = true;
|
|
294
332
|
}
|
|
333
|
+
|
|
295
334
|
if (loc.specFile && file.endsWith(loc.specFile)) {
|
|
296
335
|
loc.stepLine = line;
|
|
297
336
|
}
|
|
298
337
|
}
|
|
299
338
|
|
|
300
339
|
// Fallback: If header regex failed but we found a root file that looks like a test
|
|
301
|
-
if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.'))) {
|
|
340
|
+
if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.'))) {
|
|
302
341
|
loc.specFile = loc.rootFile;
|
|
303
342
|
loc.testLine = loc.rootLine;
|
|
304
343
|
}
|
|
@@ -307,18 +346,20 @@ function extractFailureLocation(logText) {
|
|
|
307
346
|
return null;
|
|
308
347
|
}
|
|
309
348
|
|
|
349
|
+
// --- COLORS ---
|
|
350
|
+
const C = {
|
|
351
|
+
RESET: "\x1b[0m",
|
|
352
|
+
BRIGHT: "\x1b[1m",
|
|
353
|
+
RED: "\x1b[31m",
|
|
354
|
+
GREEN: "\x1b[32m",
|
|
355
|
+
YELLOW: "\x1b[33m",
|
|
356
|
+
CYAN: "\x1b[36m",
|
|
357
|
+
BLUE: "\x1b[34m",
|
|
358
|
+
GRAY: "\x1b[90m",
|
|
359
|
+
WHITE: "\x1b[37m"
|
|
360
|
+
};
|
|
361
|
+
|
|
310
362
|
function printDetailedFix(fixText, location, sourceCode = null, isApplied = false) {
|
|
311
|
-
const C = {
|
|
312
|
-
RESET: "\x1b[0m",
|
|
313
|
-
BRIGHT: "\x1b[1m",
|
|
314
|
-
RED: "\x1b[31m",
|
|
315
|
-
GREEN: "\x1b[32m",
|
|
316
|
-
YELLOW: "\x1b[33m",
|
|
317
|
-
CYAN: "\x1b[36m",
|
|
318
|
-
BLUE: "\x1b[34m",
|
|
319
|
-
GRAY: "\x1b[90m",
|
|
320
|
-
WHITE: "\x1b[37m"
|
|
321
|
-
};
|
|
322
363
|
|
|
323
364
|
let fixCode = fixText;
|
|
324
365
|
let explanation = null;
|
|
@@ -419,16 +460,6 @@ function printDetailedFix(fixText, location, sourceCode = null, isApplied = fals
|
|
|
419
460
|
* Enforces tier limits and calculates deduplicated results.
|
|
420
461
|
*/
|
|
421
462
|
async function runDoctor(argv) {
|
|
422
|
-
const C = {
|
|
423
|
-
RESET: "\x1b[0m",
|
|
424
|
-
BRIGHT: "\x1b[1m",
|
|
425
|
-
GREEN: "\x1b[32m",
|
|
426
|
-
YELLOW: "\x1b[33m",
|
|
427
|
-
RED: "\x1b[31m",
|
|
428
|
-
CYAN: "\x1b[36m",
|
|
429
|
-
GRAY: "\x1b[90m"
|
|
430
|
-
};
|
|
431
|
-
|
|
432
463
|
console.log(`\n${C.BRIGHT}๐จโโ๏ธ DeFlake Doctor - Diagnostic Tool${C.RESET}\n`);
|
|
433
464
|
|
|
434
465
|
// 1. Environment Info & Version Check
|
|
@@ -577,15 +608,23 @@ async function runDoctor(argv) {
|
|
|
577
608
|
// 5. API Connectivity
|
|
578
609
|
console.log(`${C.BRIGHT}Checking Connectivity:${C.RESET}`);
|
|
579
610
|
const client = new DeFlakeClient(argv.apiUrl, apiKey);
|
|
580
|
-
|
|
581
|
-
console.log(` ${C.GRAY}(URL: ${client.apiUrl}, Key length: ${client.apiKey ? client.apiKey.length : 0})${C.RESET}`);
|
|
611
|
+
|
|
582
612
|
try {
|
|
583
613
|
process.stdout.write(` โณ Pinging DeFlake API... `);
|
|
584
|
-
const
|
|
585
|
-
|
|
614
|
+
const result = await client.getUsage();
|
|
615
|
+
|
|
616
|
+
if (result.status === 'success') {
|
|
617
|
+
const usage = result.data;
|
|
586
618
|
process.stdout.write(`\r โ
API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
|
|
619
|
+
console.log(` Quota: ${usage.usage}/${usage.limit}`);
|
|
587
620
|
} else {
|
|
588
|
-
process.stdout.write(`\r โ ${C.RED}API Connectivity Failed
|
|
621
|
+
process.stdout.write(`\r โ ${C.RED}API Connectivity Failed${C.RESET} \n`);
|
|
622
|
+
if (result.code === 401 || result.code === 403) {
|
|
623
|
+
console.log(` Reason: API Key is invalid (Status ${result.code}).`);
|
|
624
|
+
} else {
|
|
625
|
+
console.log(` Reason: ${result.message} (Code: ${result.code || 'UNKNOWN'})`);
|
|
626
|
+
}
|
|
627
|
+
console.log(` API URL: ${client.apiUrl}`);
|
|
589
628
|
}
|
|
590
629
|
} catch (error) {
|
|
591
630
|
process.stdout.write(`\r โ ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
|
|
@@ -648,24 +687,18 @@ async function applySelfHealing(result) {
|
|
|
648
687
|
}
|
|
649
688
|
|
|
650
689
|
async function analyzeFailures(artifacts, fullLog, client) {
|
|
651
|
-
const C = {
|
|
652
|
-
RESET: "\x1b[0m",
|
|
653
|
-
YELLOW: "\x1b[33m",
|
|
654
|
-
RED: "\x1b[31m",
|
|
655
|
-
GRAY: "\x1b[90m"
|
|
656
|
-
};
|
|
657
|
-
|
|
658
690
|
if (artifacts.length === 0) {
|
|
659
691
|
console.log("โ ๏ธ No error artifacts found.");
|
|
660
692
|
return;
|
|
661
693
|
}
|
|
662
694
|
|
|
663
695
|
// 1. Check Quota / Tier
|
|
664
|
-
const
|
|
696
|
+
const result = await client.getUsage();
|
|
665
697
|
let limit = 100; // Default safety cap
|
|
666
698
|
let tier = 'unknown';
|
|
667
699
|
|
|
668
|
-
if (
|
|
700
|
+
if (result.status === 'success') {
|
|
701
|
+
const usage = result.data;
|
|
669
702
|
tier = usage.tier || 'free';
|
|
670
703
|
if (tier === 'free') limit = 5;
|
|
671
704
|
else if (tier === 'pro') limit = 50;
|
|
@@ -708,21 +741,27 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
708
741
|
}
|
|
709
742
|
}
|
|
710
743
|
|
|
711
|
-
|
|
712
|
-
|
|
744
|
+
const displayName = (art.name || 'Unknown Artifact').substring(0, 40);
|
|
745
|
+
process.stdout.write(`\rโณ Analyzing ${displayName}... `);
|
|
713
746
|
|
|
714
|
-
|
|
715
|
-
|
|
747
|
+
try {
|
|
748
|
+
const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name);
|
|
749
|
+
|
|
750
|
+
if (result && result.status === 'success') {
|
|
751
|
+
results.push(result);
|
|
716
752
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
753
|
+
// If --fix is enabled, apply it immediately for ALL tiers
|
|
754
|
+
if (argv.fix) {
|
|
755
|
+
await applySelfHealing(result);
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
console.log(`\n ${C.RED}โ Analysis failed for ${displayName}:${C.RESET} ${result?.detail || 'Check server status'}`);
|
|
720
759
|
}
|
|
721
|
-
}
|
|
722
|
-
console.log(`\n ${C.RED}โ
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.log(`\n ${C.RED}โ Error analyzing ${displayName}:${C.RESET} ${err.message}`);
|
|
723
762
|
}
|
|
724
763
|
}
|
|
725
|
-
|
|
764
|
+
console.log("\rโ
Analysis complete. ");
|
|
726
765
|
|
|
727
766
|
// GROUPING & PRINTING
|
|
728
767
|
const groups = {};
|
|
@@ -736,20 +775,38 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
736
775
|
groups[key].tests.push(res.testName);
|
|
737
776
|
}
|
|
738
777
|
|
|
739
|
-
const sortedKeys = Object.keys(groups)
|
|
740
|
-
const lineA = parseInt(groups[a].location?.rootLine) || 0;
|
|
741
|
-
const lineB = parseInt(groups[b].location?.rootLine) || 0;
|
|
742
|
-
return lineA - lineB;
|
|
743
|
-
});
|
|
778
|
+
const sortedKeys = Object.keys(groups);
|
|
744
779
|
|
|
745
|
-
|
|
746
|
-
|
|
780
|
+
if (sortedKeys.length > 0) {
|
|
781
|
+
for (const key of sortedKeys) {
|
|
782
|
+
printDetailedFix(groups[key].fix, groups[key].location, groups[key].sourceCode, argv.fix);
|
|
783
|
+
}
|
|
747
784
|
}
|
|
748
785
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
786
|
+
// SUMMARY
|
|
787
|
+
console.log(`\n${C.BRIGHT}๐ DeFlake Summary:${C.RESET}`);
|
|
788
|
+
console.log(` - Failures analyzed: ${batchArtifacts.length}`);
|
|
789
|
+
console.log(` - Fixes suggested: ${results.length}`);
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
// Fetch updated usage for clearer reporting
|
|
793
|
+
const updatedUsage = await client.getUsage();
|
|
794
|
+
if (updatedUsage.status === 'success') {
|
|
795
|
+
const u = updatedUsage.data;
|
|
796
|
+
const remaining = u.limit - u.usage;
|
|
797
|
+
console.log(` - Quota remaining: ${C.GREEN}${remaining}/${u.limit}${C.RESET}`);
|
|
798
|
+
}
|
|
799
|
+
} catch (e) {
|
|
800
|
+
// usage fetch failed is non-critical
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (results.length === 0) {
|
|
752
804
|
console.log(`${C.GRAY}โน๏ธ DeFlake analyzed the logs but couldn't find a confident fix for these errors.${C.RESET}`);
|
|
805
|
+
if (!fullLog) {
|
|
806
|
+
console.log(`${C.YELLOW}โ ๏ธ Tip: Ensure your test runner output is being captured correctly.${C.RESET}`);
|
|
807
|
+
}
|
|
808
|
+
} else if (!argv.fix) {
|
|
809
|
+
console.log(`\n${C.BRIGHT}๐ก Tip: Use ${C.CYAN}--fix${C.RESET}${C.BRIGHT} to automatically apply these suggested fixes next time.${C.RESET}`);
|
|
753
810
|
}
|
|
754
811
|
}
|
|
755
812
|
|
package/client.js
CHANGED
|
@@ -11,6 +11,7 @@ class DeFlakeClient {
|
|
|
11
11
|
this.apiUrl = apiUrl || process.env.DEFLAKE_API_URL || this.productionUrl;
|
|
12
12
|
this.apiKey = apiKey || process.env.DEFLAKE_API_KEY;
|
|
13
13
|
this.projectName = this.detectProjectName();
|
|
14
|
+
this.framework = this.detectFramework();
|
|
14
15
|
|
|
15
16
|
if (!this.apiKey) {
|
|
16
17
|
// We no longer exit here to allow diagnostic tools (like 'doctor') to run.
|
|
@@ -39,23 +40,37 @@ class DeFlakeClient {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
detectFramework() {
|
|
44
|
+
// 1. Manual override via environment variable
|
|
45
|
+
if (process.env.DEFLAKE_FRAMEWORK) return process.env.DEFLAKE_FRAMEWORK.toLowerCase();
|
|
46
|
+
|
|
47
|
+
// 2. Automated detection via configuration files
|
|
48
|
+
if (fs.existsSync('cypress.config.js') || fs.existsSync('cypress.config.ts') || fs.existsSync('cypress')) return 'cypress';
|
|
49
|
+
if (fs.existsSync('playwright.config.js') || fs.existsSync('playwright.config.ts')) return 'playwright';
|
|
50
|
+
if (fs.existsSync('wdio.conf.js') || fs.existsSync('wdio.conf.ts')) return 'webdriverio';
|
|
51
|
+
return 'generic';
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
async getUsage() {
|
|
43
55
|
try {
|
|
44
|
-
//
|
|
45
|
-
const usageUrl = this.apiUrl.replace(
|
|
56
|
+
// Robust replacement to avoid mangling host or protocol
|
|
57
|
+
const usageUrl = this.apiUrl.replace(/\/deflake$/, '/user/usage');
|
|
46
58
|
const response = await axios.get(usageUrl, {
|
|
47
59
|
headers: {
|
|
48
60
|
'X-API-KEY': this.apiKey,
|
|
49
61
|
'X-Project-Name': this.projectName
|
|
50
62
|
}
|
|
51
63
|
});
|
|
52
|
-
return response.data;
|
|
64
|
+
return { status: 'success', data: response.data };
|
|
53
65
|
} catch (error) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
let message = error.message;
|
|
67
|
+
let code = error.code;
|
|
68
|
+
if (error.response) {
|
|
69
|
+
code = error.response.status;
|
|
70
|
+
message = error.response.data?.detail || error.response.statusText;
|
|
71
|
+
if (typeof message === 'object') message = JSON.stringify(message);
|
|
57
72
|
}
|
|
58
|
-
return
|
|
73
|
+
return { status: 'error', message, code };
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
76
|
|
|
@@ -66,13 +81,25 @@ class DeFlakeClient {
|
|
|
66
81
|
if (!fs.existsSync(htmlPath)) throw new Error(`HTML file not found: ${htmlPath}`);
|
|
67
82
|
|
|
68
83
|
const logContent = fs.readFileSync(logPath, 'utf8');
|
|
69
|
-
|
|
84
|
+
|
|
85
|
+
// Handle binary artifacts (Screenshots) vs Text artifacts (HTML/MD)
|
|
86
|
+
const isImage = htmlPath.toLowerCase().endsWith('.png');
|
|
87
|
+
let htmlContent = '';
|
|
88
|
+
|
|
89
|
+
if (isImage) {
|
|
90
|
+
// Read as base64 for screenshots
|
|
91
|
+
htmlContent = fs.readFileSync(htmlPath, 'base64');
|
|
92
|
+
} else {
|
|
93
|
+
// Read as utf-8 for HTML/MD
|
|
94
|
+
htmlContent = fs.readFileSync(htmlPath, 'utf8');
|
|
95
|
+
}
|
|
70
96
|
|
|
71
97
|
const payload = {
|
|
72
98
|
error_log: logContent || "",
|
|
73
99
|
html_snapshot: htmlContent || "",
|
|
74
100
|
failing_line: failureLocation ? `Line ${failureLocation.rootLine}` : "",
|
|
75
|
-
source_code: sourceCode || ""
|
|
101
|
+
source_code: sourceCode || "",
|
|
102
|
+
framework: this.framework
|
|
76
103
|
};
|
|
77
104
|
|
|
78
105
|
const response = await axios.post(this.apiUrl, payload, {
|
package/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// Mocking dependencies to test the function in isolation
|
|
5
|
+
const process_cwd = () => '/Users/hugo/Documents/cypress-poc';
|
|
6
|
+
const path_resolve = (...args) => path.join(...args);
|
|
7
|
+
const path_isAbsolute = (p) => path.isAbsolute(p);
|
|
8
|
+
|
|
9
|
+
// Import the function from cli.js
|
|
10
|
+
// Since it's a script with side effects, we'll extract just the function or require it carefully.
|
|
11
|
+
// To keep it simple, I'll copy the function here for unit test, but use the real dependencies.
|
|
12
|
+
|
|
13
|
+
function extractFailureLocation(logText, cwd = '/Users/hugo/Documents/cypress-poc') {
|
|
14
|
+
if (!logText) return null;
|
|
15
|
+
const loc = {
|
|
16
|
+
specFile: null,
|
|
17
|
+
testLine: null,
|
|
18
|
+
rootFile: null,
|
|
19
|
+
fullRootPath: null,
|
|
20
|
+
rootLine: null,
|
|
21
|
+
stepLine: null
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const projectName = path.basename(cwd);
|
|
25
|
+
|
|
26
|
+
// Updated regex to be more flexible with arrows and spaces, and support .cy files
|
|
27
|
+
const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy)\.(?:ts|js)):(\d+):(\d+)/m);
|
|
28
|
+
if (testMatch) {
|
|
29
|
+
loc.specFile = testMatch[1];
|
|
30
|
+
loc.testLine = testMatch[2];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Stack Trace Regex - Modified to handle Cypress URLs and webpack paths
|
|
34
|
+
const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
|
|
35
|
+
let match;
|
|
36
|
+
let foundRoot = false;
|
|
37
|
+
|
|
38
|
+
while ((match = stackRegex.exec(logText)) !== null) {
|
|
39
|
+
let file = match[1];
|
|
40
|
+
const line = match[2];
|
|
41
|
+
|
|
42
|
+
// Clean Cypress/Browser URLs to just the relative path if possible
|
|
43
|
+
if (file.includes('__cypress/runner')) continue;
|
|
44
|
+
|
|
45
|
+
if (file.includes('webpack:///')) {
|
|
46
|
+
file = file.split('webpack:///')[1];
|
|
47
|
+
} else if (file.includes('webpack://')) {
|
|
48
|
+
file = file.split('webpack://')[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Strip project name if it's the first segment (common in Cypress/Webpack logs)
|
|
52
|
+
if (file.startsWith(projectName + '/')) {
|
|
53
|
+
file = file.substring(projectName.length + 1);
|
|
54
|
+
} else if (file.startsWith('./' + projectName + '/')) {
|
|
55
|
+
file = file.substring(projectName.length + 3);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
file = file.replace(/^\.\//, '');
|
|
59
|
+
|
|
60
|
+
if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner')) {
|
|
61
|
+
loc.rootFile = file.split(/[/\\]/).pop();
|
|
62
|
+
loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(cwd, file);
|
|
63
|
+
loc.rootLine = line;
|
|
64
|
+
foundRoot = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (loc.specFile && (file.endsWith(loc.specFile) || file === loc.specFile)) {
|
|
68
|
+
loc.stepLine = line;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback: If header regex failed but we found a root file that looks like a test
|
|
73
|
+
if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.'))) {
|
|
74
|
+
loc.specFile = loc.rootFile;
|
|
75
|
+
loc.testLine = loc.rootLine;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (loc.specFile || loc.rootFile) return loc;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// TEST CASES
|
|
83
|
+
const testLogs = [
|
|
84
|
+
{
|
|
85
|
+
name: "Cypress Webpack Trace",
|
|
86
|
+
log: `API - Reqres
|
|
87
|
+
1) authenticates and returns token
|
|
88
|
+
From Your Spec Code:
|
|
89
|
+
at AuthClient.login (webpack://cypress-poc/./api/clients/AuthClient.ts:17:14)
|
|
90
|
+
at Context.eval (webpack://cypress-poc/./cypress/e2e/api/auth.api.cy.ts:12:15)`,
|
|
91
|
+
expected: {
|
|
92
|
+
rootFile: 'AuthClient.ts',
|
|
93
|
+
fullRootPath: '/Users/hugo/Documents/cypress-poc/api/clients/AuthClient.ts',
|
|
94
|
+
rootLine: '17'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "Cypress Negative Scenario",
|
|
99
|
+
log: ` 1) API - Reqres negative scenarios
|
|
100
|
+
returns 400 when password is missing:
|
|
101
|
+
|
|
102
|
+
AssertionError: expected 403 to equal 400
|
|
103
|
+
at Context.eval (webpack://cypress-poc/./cypress/e2e/api/auth.negative.api.cy.ts:11:33)`,
|
|
104
|
+
expected: {
|
|
105
|
+
rootFile: 'auth.negative.api.cy.ts',
|
|
106
|
+
fullRootPath: '/Users/hugo/Documents/cypress-poc/cypress/e2e/api/auth.negative.api.cy.ts',
|
|
107
|
+
rootLine: '11'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
console.log("๐งช Running DeFlake Path Parsing Tests...\n");
|
|
113
|
+
let passed = 0;
|
|
114
|
+
testLogs.forEach(t => {
|
|
115
|
+
const result = extractFailureLocation(t.log);
|
|
116
|
+
console.log(`Test: ${t.name}`);
|
|
117
|
+
let success = true;
|
|
118
|
+
for (const key in t.expected) {
|
|
119
|
+
if (result[key] !== t.expected[key]) {
|
|
120
|
+
console.log(` โ FAIL: ${key} expected "${t.expected[key]}", got "${result[key]}"`);
|
|
121
|
+
success = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (success) {
|
|
125
|
+
console.log(` โ
PASS`);
|
|
126
|
+
passed++;
|
|
127
|
+
}
|
|
128
|
+
console.log("");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log(`Summary: ${passed}/${testLogs.length} tests passed.`);
|
|
132
|
+
if (passed < testLogs.length) process.exit(1);
|