deflake 1.0.8 ā 1.0.10
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 +65 -25
- package/client.js +10 -7
- package/package.json +1 -1
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,44 @@ function extractFailureLocation(logText) {
|
|
|
272
288
|
stepLine: null
|
|
273
289
|
};
|
|
274
290
|
|
|
275
|
-
// Updated regex to be more flexible with arrows and spaces
|
|
276
|
-
const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(
|
|
291
|
+
// Updated regex to be more flexible with arrows and spaces, and support .cy files
|
|
292
|
+
const testMatch = logText.match(/^\s*\d+\)\s+\[.*?\]\s+.+?\s+(.*?\.(?:spec|cy)\.(?:ts|js)):(\d+):(\d+)/m);
|
|
277
293
|
if (testMatch) {
|
|
278
294
|
loc.specFile = testMatch[1];
|
|
279
295
|
loc.testLine = testMatch[2];
|
|
280
296
|
}
|
|
281
297
|
|
|
282
|
-
|
|
298
|
+
// Stack Trace Regex - Modified to handle Cypress URLs and webpack paths
|
|
299
|
+
const stackRegex = /at\s+(?:.*? \()?((?:https?:\/\/.*?\/|webpack:\/\/\/|[\/~\\]|\.?\.\/|[\w_\-]+\/).*?):(\d+):(\d+)\)?/g;
|
|
283
300
|
let match;
|
|
284
301
|
let foundRoot = false;
|
|
285
302
|
|
|
286
303
|
while ((match = stackRegex.exec(logText)) !== null) {
|
|
287
|
-
|
|
304
|
+
let file = match[1];
|
|
288
305
|
const line = match[2];
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
306
|
+
|
|
307
|
+
// Clean Cypress/Browser URLs to just the relative path if possible
|
|
308
|
+
if (file.includes('__cypress/runner')) continue; // Skip internal runner files
|
|
309
|
+
if (file.includes('webpack:///')) {
|
|
310
|
+
file = file.split('webpack:///')[1].replace(/^\.\//, '');
|
|
311
|
+
} else if (file.includes('webpack://')) {
|
|
312
|
+
file = file.split('webpack://')[1].replace(/^\.\//, '');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!foundRoot && !file.includes('node_modules') && !file.includes('cypress_runner')) {
|
|
316
|
+
loc.rootFile = file.split(/[/\\]/).pop();
|
|
317
|
+
loc.fullRootPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
|
|
292
318
|
loc.rootLine = line;
|
|
293
319
|
foundRoot = true;
|
|
294
320
|
}
|
|
321
|
+
|
|
295
322
|
if (loc.specFile && file.endsWith(loc.specFile)) {
|
|
296
323
|
loc.stepLine = line;
|
|
297
324
|
}
|
|
298
325
|
}
|
|
299
326
|
|
|
300
327
|
// 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.'))) {
|
|
328
|
+
if (!loc.specFile && loc.rootFile && (loc.rootFile.includes('.spec.') || loc.rootFile.includes('.test.') || loc.rootFile.includes('.cy.'))) {
|
|
302
329
|
loc.specFile = loc.rootFile;
|
|
303
330
|
loc.testLine = loc.rootLine;
|
|
304
331
|
}
|
|
@@ -577,15 +604,23 @@ async function runDoctor(argv) {
|
|
|
577
604
|
// 5. API Connectivity
|
|
578
605
|
console.log(`${C.BRIGHT}Checking Connectivity:${C.RESET}`);
|
|
579
606
|
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}`);
|
|
607
|
+
|
|
582
608
|
try {
|
|
583
609
|
process.stdout.write(` ā³ Pinging DeFlake API... `);
|
|
584
|
-
const
|
|
585
|
-
|
|
610
|
+
const result = await client.getUsage();
|
|
611
|
+
|
|
612
|
+
if (result.status === 'success') {
|
|
613
|
+
const usage = result.data;
|
|
586
614
|
process.stdout.write(`\r ā
API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
|
|
615
|
+
console.log(` Quota: ${usage.usage}/${usage.limit}`);
|
|
587
616
|
} else {
|
|
588
|
-
process.stdout.write(`\r ā ${C.RED}API Connectivity Failed
|
|
617
|
+
process.stdout.write(`\r ā ${C.RED}API Connectivity Failed${C.RESET} \n`);
|
|
618
|
+
if (result.code === 401 || result.code === 403) {
|
|
619
|
+
console.log(` Reason: API Key is invalid (Status ${result.code}).`);
|
|
620
|
+
} else {
|
|
621
|
+
console.log(` Reason: ${result.message} (Code: ${result.code || 'UNKNOWN'})`);
|
|
622
|
+
}
|
|
623
|
+
console.log(` API URL: ${client.apiUrl}`);
|
|
589
624
|
}
|
|
590
625
|
} catch (error) {
|
|
591
626
|
process.stdout.write(`\r ā ${C.RED}API Connectivity Error: ${error.message}${C.RESET}\n`);
|
|
@@ -661,11 +696,12 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
661
696
|
}
|
|
662
697
|
|
|
663
698
|
// 1. Check Quota / Tier
|
|
664
|
-
const
|
|
699
|
+
const result = await client.getUsage();
|
|
665
700
|
let limit = 100; // Default safety cap
|
|
666
701
|
let tier = 'unknown';
|
|
667
702
|
|
|
668
|
-
if (
|
|
703
|
+
if (result.status === 'success') {
|
|
704
|
+
const usage = result.data;
|
|
669
705
|
tier = usage.tier || 'free';
|
|
670
706
|
if (tier === 'free') limit = 5;
|
|
671
707
|
else if (tier === 'pro') limit = 50;
|
|
@@ -736,20 +772,24 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
736
772
|
groups[key].tests.push(res.testName);
|
|
737
773
|
}
|
|
738
774
|
|
|
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
|
-
});
|
|
775
|
+
const sortedKeys = Object.keys(groups);
|
|
744
776
|
|
|
745
|
-
|
|
746
|
-
|
|
777
|
+
if (sortedKeys.length > 0) {
|
|
778
|
+
for (const key of sortedKeys) {
|
|
779
|
+
printDetailedFix(groups[key].fix, groups[key].location, groups[key].sourceCode, argv.fix);
|
|
780
|
+
}
|
|
747
781
|
}
|
|
748
782
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
783
|
+
// SUMMARY
|
|
784
|
+
console.log(`\nš DeFlake Summary:`);
|
|
785
|
+
console.log(` - Failures analyzed: ${batchArtifacts.length}`);
|
|
786
|
+
console.log(` - Fixes suggested: ${results.length}`);
|
|
787
|
+
|
|
788
|
+
if (results.length === 0) {
|
|
752
789
|
console.log(`${C.GRAY}ā¹ļø DeFlake analyzed the logs but couldn't find a confident fix for these errors.${C.RESET}`);
|
|
790
|
+
if (!fullLog) {
|
|
791
|
+
console.log(`${C.YELLOW}ā ļø Tip: Ensure your test runner output is being captured correctly.${C.RESET}`);
|
|
792
|
+
}
|
|
753
793
|
}
|
|
754
794
|
}
|
|
755
795
|
|
package/client.js
CHANGED
|
@@ -41,21 +41,24 @@ class DeFlakeClient {
|
|
|
41
41
|
|
|
42
42
|
async getUsage() {
|
|
43
43
|
try {
|
|
44
|
-
//
|
|
45
|
-
const usageUrl = this.apiUrl.replace(
|
|
44
|
+
// Robust replacement to avoid mangling host or protocol
|
|
45
|
+
const usageUrl = this.apiUrl.replace(/\/deflake$/, '/user/usage');
|
|
46
46
|
const response = await axios.get(usageUrl, {
|
|
47
47
|
headers: {
|
|
48
48
|
'X-API-KEY': this.apiKey,
|
|
49
49
|
'X-Project-Name': this.projectName
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
return response.data;
|
|
52
|
+
return { status: 'success', data: response.data };
|
|
53
53
|
} catch (error) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
let message = error.message;
|
|
55
|
+
let code = error.code;
|
|
56
|
+
if (error.response) {
|
|
57
|
+
code = error.response.status;
|
|
58
|
+
message = error.response.data?.detail || error.response.statusText;
|
|
59
|
+
if (typeof message === 'object') message = JSON.stringify(message);
|
|
57
60
|
}
|
|
58
|
-
return
|
|
61
|
+
return { status: 'error', message, code };
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|