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.
@@ -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.passed !== undefined) { log(` ${c.green}✔ ${r.passed} passed${c.reset}`); totalPassed += r.passed; }
232
- if (r.failed > 0) { log(` ${c.red}✖ ${r.failed} failed${c.reset}`); totalFailed += r.failed; }
233
- if (r.skipped > 0) { log(` ${c.dim}○ ${r.skipped} skipped${c.reset}`); totalSkipped += r.skipped; }
234
- if (r.suites) { log(` ${c.dim}${r.suites}${c.reset}`); }
235
- if (r.status === 'skipped' && r.output) { warn(r.output); }
236
- if (r.status === 'error' && r.error) { fail(r.error.slice(0, 200)); }
237
-
238
- // Print first failed test details
239
- if (r.status === 'failed' && r.output) {
240
- const lines = r.output.split('\n').filter(l => l.includes('✖') || l.includes('FAIL') || l.includes('Error') || l.includes('expect'));
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}`);
@@ -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:7px;padding:14px 16px;margin-bottom:8px;background:#fff">
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:600;color:#1f2328;font-size:14px">
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 !== undefined ? `<span style="color:#22c55e;font-size:12px;font-weight:700">${r.passed} passed</span>` : ''}
43
- ${r.failed !== undefined && r.failed > 0 ? `<span style="color:#ef4444;font-size:12px;font-weight:700">${r.failed} failed</span>` : ''}
44
- ${r.skipped !== undefined && r.skipped > 0 ? `<span style="color:#94a3b8;font-size:12px;font-weight:700">${r.skipped} skipped</span>` : ''}
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
- ${output}${errOut}
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 output = r.output ? `\n\n<details>\n<summary>Output log</summary>\n\n\`\`\`\n${r.output.slice(-2000)}\n\`\`\`\n</details>` : '';
221
- const error = r.error ? `\n\n> **Error:** ${r.error}` : '';
222
- 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` : ''}${output}${error}`;
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_';
@@ -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 readable test breakdown
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
- for (const t of (suite.testResults || [])) {
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.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": {