project-scan 1.0.0 → 1.0.1
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/bin/project-scan.js +32 -12
- package/lib/html-report.js +47 -9
- package/lib/md-report.js +24 -3
- package/lib/test-runner.js +17 -4
- package/package.json +1 -1
package/bin/project-scan.js
CHANGED
|
@@ -228,20 +228,40 @@ function printTestResults(results) {
|
|
|
228
228
|
const ms = r.duration ? `${c.dim} (${(r.duration/1000).toFixed(2)}s)${c.reset}` : '';
|
|
229
229
|
log(`\n ${icon}${c.reset} ${c.bold}${r.suite}${c.reset}${ms}`);
|
|
230
230
|
|
|
231
|
-
if (r.
|
|
232
|
-
if (r.
|
|
233
|
-
if (r.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (r.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (lines.length > 0) {
|
|
242
|
-
log(` ${c.dim}${c.red}${lines.slice(0, 5).join('\n ')}${c.reset}`);
|
|
231
|
+
if (r.suites) { log(` ${c.dim}${r.suites}${c.reset}`); }
|
|
232
|
+
if (r.status === 'skipped' && r.output) { warn(r.output); }
|
|
233
|
+
if (r.status === 'error' && r.error) { fail(r.error.slice(0, 200)); }
|
|
234
|
+
|
|
235
|
+
// ── Per-test-case LeetCode style list ────────────────────────────────────
|
|
236
|
+
if (r.testCases && r.testCases.length > 0) {
|
|
237
|
+
// Group by file
|
|
238
|
+
const byFile = {};
|
|
239
|
+
for (const tc of r.testCases) {
|
|
240
|
+
(byFile[tc.file] = byFile[tc.file] || []).push(tc);
|
|
243
241
|
}
|
|
242
|
+
for (const [file, cases] of Object.entries(byFile)) {
|
|
243
|
+
log(`\n ${c.dim}📄 ${file}${c.reset}`);
|
|
244
|
+
for (const tc of cases) {
|
|
245
|
+
const tcIcon = tc.status === 'passed' ? `${c.green} ✔` : tc.status === 'failed' ? `${c.red} ✖` : `${c.dim} ○`;
|
|
246
|
+
const tcMs = tc.duration > 0 ? `${c.dim} ${tc.duration}ms${c.reset}` : '';
|
|
247
|
+
log(` ${tcIcon}${c.reset} ${tc.name}${tcMs}`);
|
|
248
|
+
if (tc.error) {
|
|
249
|
+
// First meaningful error line
|
|
250
|
+
const errLine = tc.error.split('\n').find(l => l.trim().startsWith('expect') || l.trim().startsWith('Error') || l.includes('Expected') || l.includes('Received'));
|
|
251
|
+
if (errLine) log(` ${c.red}${c.dim}${errLine.trim()}${c.reset}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// Fallback summary for non-Jest runners
|
|
257
|
+
if (r.passed > 0) log(` ${c.green}✔ ${r.passed} passed${c.reset}`);
|
|
258
|
+
if (r.failed > 0) log(` ${c.red}✖ ${r.failed} failed${c.reset}`);
|
|
259
|
+
if (r.skipped > 0) log(` ${c.dim}○ ${r.skipped} skipped${c.reset}`);
|
|
244
260
|
}
|
|
261
|
+
|
|
262
|
+
totalPassed += r.passed || 0;
|
|
263
|
+
totalFailed += r.failed || 0;
|
|
264
|
+
totalSkipped += r.skipped || 0;
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
log(`\n ${c.bold}Total: ${c.green}${totalPassed} passed${c.reset} · ${c.red}${totalFailed} failed${c.reset} · ${c.dim}${totalSkipped} skipped${c.reset}`);
|
package/lib/html-report.js
CHANGED
|
@@ -24,28 +24,66 @@ function generateHTML(scanResult, testResults) {
|
|
|
24
24
|
const card = (content, extra = '') =>
|
|
25
25
|
`<div style="background:#f7f8fa;border:1px solid #e5e7eb;border-radius:8px;padding:16px 18px;margin-bottom:10px${extra}">${content}</div>`;
|
|
26
26
|
|
|
27
|
-
// Test suite rows
|
|
27
|
+
// Test suite rows — LeetCode style per-test breakdown
|
|
28
28
|
const testRows = testResults.map(r => {
|
|
29
29
|
const icon = r.status === 'passed' ? '✔' : r.status === 'failed' ? '✖' : r.status === 'error' ? '⚠' : '○';
|
|
30
30
|
const color = r.status === 'passed' ? '#22c55e' : r.status === 'failed' ? '#ef4444' : r.status === 'error' ? '#f59e0b' : '#94a3b8';
|
|
31
31
|
const ms = r.duration ? `${(r.duration / 1000).toFixed(2)}s` : '—';
|
|
32
|
-
const output = r.output ? `<pre style="margin:8px 0 0;padding:10px;background:#1e1e2e;color:#cdd6f4;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap;max-height:220px;overflow-y:auto">${esc(r.output.slice(-2500))}</pre>` : '';
|
|
33
|
-
const errOut = r.error ? `<pre style="margin:8px 0 0;padding:10px;background:#2a0a0a;color:#fca5a5;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap">${esc(r.error)}</pre>` : '';
|
|
34
32
|
const suitesMeta = r.suites ? `<span style="margin-left:8px;color:#57606a;font-size:12px">(${r.suites})</span>` : '';
|
|
33
|
+
const errOut = r.error ? `<pre style="margin:8px 0 0;padding:10px;background:#2a0a0a;color:#fca5a5;border-radius:5px;font-size:11px;white-space:pre-wrap">${esc(r.error)}</pre>` : '';
|
|
34
|
+
|
|
35
|
+
// Per-test-case LeetCode grid
|
|
36
|
+
let caseGrid = '';
|
|
37
|
+
if (r.testCases && r.testCases.length > 0) {
|
|
38
|
+
// Group by file
|
|
39
|
+
const byFile = {};
|
|
40
|
+
for (const tc of r.testCases) {
|
|
41
|
+
(byFile[tc.file] = byFile[tc.file] || []).push(tc);
|
|
42
|
+
}
|
|
43
|
+
const fileBlocks = Object.entries(byFile).map(([file, cases]) => {
|
|
44
|
+
const rows = cases.map(tc => {
|
|
45
|
+
const tcColor = tc.status === 'passed' ? '#22c55e' : tc.status === 'failed' ? '#ef4444' : '#94a3b8';
|
|
46
|
+
const tcBg = tc.status === 'passed' ? '#f0fdf4' : tc.status === 'failed' ? '#fef2f2' : '#f8fafc';
|
|
47
|
+
const tcBorder = tc.status === 'passed' ? '#bbf7d0' : tc.status === 'failed' ? '#fecaca' : '#e2e8f0';
|
|
48
|
+
const tcIcon = tc.status === 'passed' ? '✔' : tc.status === 'failed' ? '✖' : '○';
|
|
49
|
+
const tcMs = tc.duration > 0 ? `<span style="color:#94a3b8;font-size:11px;margin-left:auto;padding-left:8px;white-space:nowrap">${tc.duration}ms</span>` : '';
|
|
50
|
+
const errBlock = tc.error ? `<pre style="margin:6px 0 0;padding:8px 10px;background:#1e1e2e;color:#fca5a5;border-radius:4px;font-size:10.5px;white-space:pre-wrap;overflow-x:auto">${esc(tc.error)}</pre>` : '';
|
|
51
|
+
return `
|
|
52
|
+
<div style="display:flex;flex-direction:column;border:1px solid ${tcBorder};border-radius:6px;padding:8px 12px;background:${tcBg}">
|
|
53
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
54
|
+
<span style="color:${tcColor};font-size:14px;font-weight:700;flex-shrink:0">${tcIcon}</span>
|
|
55
|
+
<span style="font-size:12.5px;color:#1f2328;flex:1;line-height:1.4">${esc(tc.name)}</span>
|
|
56
|
+
${tcMs}
|
|
57
|
+
</div>
|
|
58
|
+
${errBlock}
|
|
59
|
+
</div>`;
|
|
60
|
+
}).join('');
|
|
61
|
+
return `
|
|
62
|
+
<div style="margin-bottom:12px">
|
|
63
|
+
<div style="font-size:11px;color:#57606a;font-family:monospace;margin-bottom:6px;padding:3px 8px;background:#f0f3f6;border-radius:4px;display:inline-block">${esc(file)}</div>
|
|
64
|
+
<div style="display:flex;flex-direction:column;gap:5px">${rows}</div>
|
|
65
|
+
</div>`;
|
|
66
|
+
}).join('');
|
|
67
|
+
caseGrid = `<div style="margin-top:12px">${fileBlocks}</div>`;
|
|
68
|
+
} else if (r.output && r.status !== 'skipped') {
|
|
69
|
+
// Fallback: raw output for non-Jest runners
|
|
70
|
+
caseGrid = `<pre style="margin:8px 0 0;padding:10px;background:#1e1e2e;color:#cdd6f4;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap;max-height:200px;overflow-y:auto">${esc(r.output.slice(-2000))}</pre>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
35
73
|
return `
|
|
36
|
-
<div style="border:1px solid #e5e7eb;border-radius:
|
|
74
|
+
<div style="border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;margin-bottom:12px;background:#fff">
|
|
37
75
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:4px">
|
|
38
|
-
<span style="font-weight:
|
|
76
|
+
<span style="font-weight:700;color:#1f2328;font-size:14px">
|
|
39
77
|
<span style="color:${color};margin-right:6px;font-size:16px">${icon}</span>${esc(r.suite)}${suitesMeta}
|
|
40
78
|
</span>
|
|
41
79
|
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
|
|
42
|
-
${r.passed
|
|
43
|
-
${r.failed
|
|
44
|
-
${r.skipped
|
|
80
|
+
${r.passed > 0 ? `<span style="color:#22c55e;font-size:12px;font-weight:700">${r.passed} passed</span>` : ''}
|
|
81
|
+
${r.failed > 0 ? `<span style="color:#ef4444;font-size:12px;font-weight:700">${r.failed} failed</span>` : ''}
|
|
82
|
+
${r.skipped > 0 ? `<span style="color:#94a3b8;font-size:12px;font-weight:700">${r.skipped} skipped</span>` : ''}
|
|
45
83
|
<span style="color:#57606a;font-size:12px">${ms}</span>
|
|
46
84
|
</div>
|
|
47
85
|
</div>
|
|
48
|
-
${
|
|
86
|
+
${caseGrid}${errOut}
|
|
49
87
|
</div>`;
|
|
50
88
|
}).join('');
|
|
51
89
|
|
package/lib/md-report.js
CHANGED
|
@@ -217,9 +217,30 @@ function generateTestsReport(scanResult, testResults) {
|
|
|
217
217
|
const suiteDetails = testResults.map(r => {
|
|
218
218
|
const icon = r.status === 'passed' ? '✅' : r.status === 'failed' ? '❌' : r.status === 'error' ? '⚠️' : '⏭️';
|
|
219
219
|
const ms = r.duration ? `${(r.duration/1000).toFixed(2)}s` : '—';
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
const error = r.error ? `\n\n> **Error:** ${r.error}` : '';
|
|
221
|
+
|
|
222
|
+
// Per-test-case rows (LeetCode style)
|
|
223
|
+
let caseTable = '';
|
|
224
|
+
if (r.testCases && r.testCases.length > 0) {
|
|
225
|
+
const byFile = {};
|
|
226
|
+
for (const tc of r.testCases) {
|
|
227
|
+
(byFile[tc.file] = byFile[tc.file] || []).push(tc);
|
|
228
|
+
}
|
|
229
|
+
const blocks = Object.entries(byFile).map(([file, cases]) => {
|
|
230
|
+
const rows = cases.map(tc => {
|
|
231
|
+
const tcIcon = tc.status === 'passed' ? '✅' : tc.status === 'failed' ? '❌' : '⏭️';
|
|
232
|
+
const tcMs = tc.duration > 0 ? `${tc.duration}ms` : '—';
|
|
233
|
+
const errLine = tc.error
|
|
234
|
+
? `\n > \`\`\`\n > ${tc.error.split('\n').slice(0,4).join('\n > ')}\n > \`\`\``
|
|
235
|
+
: '';
|
|
236
|
+
return `| ${tcIcon} | ${tc.name} | ${tcMs} |${errLine}`;
|
|
237
|
+
}).join('\n');
|
|
238
|
+
return `**\`${file}\`**\n\n| | Test Case | Duration |\n|--|-----------|----------|\n${rows}`;
|
|
239
|
+
});
|
|
240
|
+
caseTable = '\n\n' + blocks.join('\n\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return `### ${icon} ${r.suite}\n\n| Metric | Value |\n|--------|-------|\n| Status | \`${r.status}\` |\n| Passed | ${r.passed||0} |\n| Failed | ${r.failed||0} |\n| Skipped | ${r.skipped||0} |\n| Duration | ${ms} |\n${r.suites ? `| Suites | ${r.suites} |\n` : ''}${caseTable}${error}`;
|
|
223
244
|
}).join('\n\n---\n\n');
|
|
224
245
|
|
|
225
246
|
const testFilesList = (scanResult.structure.testFiles || []).slice(0, 30).map(f => `- \`${f}\``).join('\n') || '- _None found_';
|
package/lib/test-runner.js
CHANGED
|
@@ -280,17 +280,29 @@ function runJest(dir, label) {
|
|
|
280
280
|
const skipped = jResult.numPendingTests || 0;
|
|
281
281
|
const status = failed > 0 ? 'failed' : (passed > 0 ? 'passed' : 'skipped');
|
|
282
282
|
|
|
283
|
-
// Build
|
|
283
|
+
// Build structured per-test case list (LeetCode style)
|
|
284
|
+
// Jest uses 'assertionResults' per suite (not 'testResults')
|
|
285
|
+
const testCases = [];
|
|
284
286
|
const lines = [];
|
|
285
287
|
for (const suite of (jResult.testResults || [])) {
|
|
286
|
-
const relPath = path.relative(dir, suite.testFilePath);
|
|
288
|
+
const relPath = path.relative(dir, suite.testFilePath || suite.name || '');
|
|
287
289
|
lines.push(`\n 📄 ${relPath}`);
|
|
288
|
-
|
|
290
|
+
const assertions = suite.assertionResults || suite.testResults || [];
|
|
291
|
+
for (const t of assertions) {
|
|
289
292
|
const icon = t.status === 'passed' ? ' ✔' : t.status === 'failed' ? ' ✖' : ' ○';
|
|
290
|
-
lines.push(`${icon} ${t.fullName}`);
|
|
293
|
+
lines.push(`${icon} ${t.fullName || t.title}`);
|
|
291
294
|
if (t.status === 'failed' && t.failureMessages?.length) {
|
|
292
295
|
lines.push(` ${t.failureMessages[0].split('\n').slice(0, 3).join('\n ')}`);
|
|
293
296
|
}
|
|
297
|
+
testCases.push({
|
|
298
|
+
file: relPath,
|
|
299
|
+
name: t.fullName || t.title || '(unnamed)',
|
|
300
|
+
status: t.status, // 'passed' | 'failed' | 'pending'
|
|
301
|
+
duration: t.duration || 0,
|
|
302
|
+
error: t.status === 'failed' && t.failureMessages?.length
|
|
303
|
+
? t.failureMessages[0].split('\n').slice(0, 6).join('\n')
|
|
304
|
+
: null,
|
|
305
|
+
});
|
|
294
306
|
}
|
|
295
307
|
}
|
|
296
308
|
|
|
@@ -301,6 +313,7 @@ function runJest(dir, label) {
|
|
|
301
313
|
duration,
|
|
302
314
|
output: lines.join('\n'),
|
|
303
315
|
suites: jResult.numPassedTestSuites + '/' + jResult.numTotalTestSuites + ' suites passed',
|
|
316
|
+
testCases,
|
|
304
317
|
};
|
|
305
318
|
} catch { /* fall through to text parse */ }
|
|
306
319
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-scan",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Universal project summarizer & test runner — scans any project for languages, frameworks, databases, cloud services, CI/CD pipelines and runs tests. Works with JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, PHP, Ruby, Dart, C#, and more.",
|
|
5
5
|
"main": "lib/scanner.js",
|
|
6
6
|
"bin": {
|