From 2c634821ca9e7d58c22278779e0bda3bf363882a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:35:51 +0000 Subject: [PATCH 1/3] Initial plan From cd1257a1c9bbb3a0441783022072b0986b433885 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:39:06 +0000 Subject: [PATCH 2/3] Initial assessment: Understanding current codebase structure Co-authored-by: dreamquality <130073078+dreamquality@users.noreply.github.com> --- auto-detect-newman.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auto-detect-newman.html b/auto-detect-newman.html index 027381d..48e7c55 100644 --- a/auto-detect-newman.html +++ b/auto-detect-newman.html @@ -364,7 +364,7 @@

Swagger Coverage Report

🔆
-

Timestamp: 9/15/2025, 4:45:45 PM

+

Timestamp: 9/16/2025, 5:37:53 PM

API Spec: Test API

Postman Collection: Test Newman Collection

From 194464c03a2533d4a430d949ef8d56dfe5542bff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:48:30 +0000 Subject: [PATCH 3/3] Add coverage heatmap feature with --heatmap CLI flag Co-authored-by: dreamquality <130073078+dreamquality@users.noreply.github.com> --- auto-detect-newman.html | 2 +- cli.js | 43 ++-- lib/report.js | 512 ++++++++++++++++++++++++++++++++++++++- test/heatmap-cli.test.js | 113 +++++++++ test/heatmap.test.js | 178 ++++++++++++++ 5 files changed, 830 insertions(+), 18 deletions(-) create mode 100644 test/heatmap-cli.test.js create mode 100644 test/heatmap.test.js diff --git a/auto-detect-newman.html b/auto-detect-newman.html index 48e7c55..3619bd9 100644 --- a/auto-detect-newman.html +++ b/auto-detect-newman.html @@ -364,7 +364,7 @@

Swagger Coverage Report

🔆
-

Timestamp: 9/16/2025, 5:37:53 PM

+

Timestamp: 9/16/2025, 5:47:53 PM

API Spec: Test API

Postman Collection: Test Newman Collection

diff --git a/cli.js b/cli.js index c7379d7..e12b457 100644 --- a/cli.js +++ b/cli.js @@ -9,7 +9,7 @@ const { loadAndParseSpec, extractOperationsFromSpec } = require("./lib/swagger") const { loadPostmanCollection, extractRequestsFromPostman } = require("./lib/postman"); const { loadNewmanReport, extractRequestsFromNewman } = require("./lib/newman"); const { matchOperationsDetailed } = require("./lib/match"); -const { generateHtmlReport } = require("./lib/report"); +const { generateHtmlReport, generateHeatmapReport } = require("./lib/report"); const { loadExcelSpec } = require("./lib/excel"); const program = new Command(); @@ -27,9 +27,10 @@ program .option("--strict-body", "Enable strict validation of requestBody (JSON)") .option("--output ", "HTML report output file", "coverage-report.html") .option("--newman", "Treat input file as Newman run report instead of Postman collection") + .option("--heatmap", "Generate coverage heatmap (graph API with endpoints nodes, methods edges, and coverage highlighting)") .action(async (swaggerFiles, postmanFile, options) => { try { - const { verbose, strictQuery, strictBody, output, newman } = options; + const { verbose, strictQuery, strictBody, output, newman, heatmap } = options; // Parse comma-separated swagger files const files = swaggerFiles.includes(',') ? @@ -172,21 +173,33 @@ program `Multiple APIs (${allSpecNames.join(', ')})` : allSpecNames[0]; - const html = generateHtmlReport({ - coverage, - coverageItems, - meta: { - timestamp: new Date().toLocaleString(), - specName: combinedSpecName, - postmanCollectionName: collectionName, - undocumentedRequests, - apiCount: files.length, - apiNames: allSpecNames - }, - }); + const reportMeta = { + timestamp: new Date().toLocaleString(), + specName: combinedSpecName, + postmanCollectionName: collectionName, + undocumentedRequests, + apiCount: files.length, + apiNames: allSpecNames + }; + + let html; + if (heatmap) { + html = generateHeatmapReport({ + coverage, + coverageItems, + meta: reportMeta + }); + console.log(`\nHeatmap report saved to: ${output}`); + } else { + html = generateHtmlReport({ + coverage, + coverageItems, + meta: reportMeta + }); + console.log(`\nHTML report saved to: ${output}`); + } fs.writeFileSync(path.resolve(output), html, "utf8"); - console.log(`\nHTML report saved to: ${output}`); } catch (err) { console.error("Error:", err.message); process.exit(1); diff --git a/lib/report.js b/lib/report.js index 8c024ef..b9d37b0 100644 --- a/lib/report.js +++ b/lib/report.js @@ -1010,5 +1010,513 @@ function generateHtmlReport({ coverage, coverageItems, meta }) { return html; } -// Export the function -module.exports = { generateHtmlReport }; +/** + * generateHeatmapReport - creates a graph visualization showing: + * - Nodes: API endpoints + * - Edges: HTTP methods connecting endpoints + * - Coverage highlighting: color-coded based on test coverage + */ +function generateHeatmapReport({ coverage, coverageItems, meta }) { + const { timestamp, specName, postmanCollectionName, apiCount = 1, apiNames = [] } = meta; + + // Process coverage data to create nodes and edges + const nodes = []; + const edges = []; + const nodeMap = new Map(); + + // Group endpoints by path + const pathGroups = new Map(); + coverageItems.forEach(item => { + if (!pathGroups.has(item.path)) { + pathGroups.set(item.path, []); + } + pathGroups.get(item.path).push(item); + }); + + // Create nodes for each unique path + let nodeId = 0; + pathGroups.forEach((methods, path) => { + const coveredMethods = methods.filter(m => !m.unmatched); + const coverageRatio = coveredMethods.length / methods.length; + + // Color based on coverage: green for full coverage, yellow for partial, red for none + let color = '#f44336'; // red for no coverage + if (coverageRatio === 1) { + color = '#4caf50'; // green for full coverage + } else if (coverageRatio > 0) { + color = '#ff9800'; // orange for partial coverage + } + + const node = { + id: nodeId, + label: path, + path: path, + methods: methods.map(m => m.method), + coverage: coverageRatio, + color: color, + size: 20 + (methods.length * 5), // Size based on number of methods + title: `Path: ${path}\\nMethods: ${methods.map(m => m.method).join(', ')}\\nCoverage: ${(coverageRatio * 100).toFixed(1)}%` + }; + + nodes.push(node); + nodeMap.set(path, nodeId); + nodeId++; + }); + + // Create edges between paths that share similar patterns or tags + const pathArray = Array.from(pathGroups.keys()); + for (let i = 0; i < pathArray.length; i++) { + for (let j = i + 1; j < pathArray.length; j++) { + const path1 = pathArray[i]; + const path2 = pathArray[j]; + + // Connect paths that share common segments or tags + const segments1 = path1.split('/').filter(s => s); + const segments2 = path2.split('/').filter(s => s); + const commonSegments = segments1.filter(s => segments2.includes(s)); + + if (commonSegments.length > 0) { + // Create edge with weight based on common segments + edges.push({ + from: nodeMap.get(path1), + to: nodeMap.get(path2), + weight: commonSegments.length, + title: `Common segments: ${commonSegments.join(', ')}` + }); + } + } + } + + const graphData = { + nodes: nodes, + edges: edges + }; + + const html = ` + + + + + + API Coverage Heatmap - ${specName} + + + + +
+

API Coverage Heatmap

+
+ Specification: ${specName} + Collection: ${postmanCollectionName} + Generated: ${timestamp} + Overall Coverage: ${coverage.toFixed(2)}% +
+
+ +
+
+
+
+ Fully Covered +
+
+
+ Partially Covered +
+
+
+ Not Covered +
+
+ +
+ + + +
+
+ +
+
+
Loading graph...
+
+ +
+
+
${coverageItems.length}
+
Total Endpoints
+
+
+
${coverageItems.filter(item => !item.unmatched).length}
+
Covered Endpoints
+
+
+
${nodes.length}
+
Unique Paths
+
+
+
${coverage.toFixed(1)}%
+
Coverage
+
+
+ +
+

Generated by swagger-coverage-cli (Heatmap Mode)

+
+ + + + + + + +`; + + return html; +} + +// Export the functions +module.exports = { generateHtmlReport, generateHeatmapReport }; diff --git a/test/heatmap-cli.test.js b/test/heatmap-cli.test.js new file mode 100644 index 0000000..80617bf --- /dev/null +++ b/test/heatmap-cli.test.js @@ -0,0 +1,113 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +describe('CLI Heatmap Integration', () => { + const tmpDir = path.join(__dirname, '../tmp'); + const testOutputPath = path.join(tmpDir, 'heatmap-cli-test.html'); + + beforeAll(() => { + // Ensure tmp directory exists + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, { recursive: true }); + } + }); + + afterEach(() => { + // Clean up test output file + if (fs.existsSync(testOutputPath)) { + fs.unlinkSync(testOutputPath); + } + }); + + test('CLI should generate heatmap report with --heatmap flag', () => { + const swaggerFile = path.join(__dirname, 'fixtures/users-api.yaml'); + const postmanFile = path.join(__dirname, 'fixtures/test-collection.json'); + + const command = `node cli.js "${swaggerFile}" "${postmanFile}" --heatmap --output "${testOutputPath}"`; + + let stdout; + try { + stdout = execSync(command, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 30000 + }); + } catch (error) { + console.error('Command failed:', error.message); + console.error('Stdout:', error.stdout); + console.error('Stderr:', error.stderr); + throw error; + } + + // Verify command output + expect(stdout).toContain('=== Swagger Coverage Report ==='); + expect(stdout).toContain('Heatmap report saved to:'); + expect(stdout).toContain(testOutputPath); + + // Verify file was created + expect(fs.existsSync(testOutputPath)).toBe(true); + + // Verify file content + const fileContent = fs.readFileSync(testOutputPath, 'utf8'); + expect(fileContent).toContain('API Coverage Heatmap'); + expect(fileContent).toContain('API Coverage Matrix'); + expect(fileContent).toContain('Endpoint Details'); + expect(fileContent).toContain('Users API'); + expect(fileContent).toContain('Toggle Theme'); + expect(fileContent).toContain('createHeatmapMatrix'); + expect(fileContent).toContain('Generated by swagger-coverage-cli (Heatmap Mode)'); + }); + + test('CLI should generate regular report without --heatmap flag', () => { + const swaggerFile = path.join(__dirname, 'fixtures/users-api.yaml'); + const postmanFile = path.join(__dirname, 'fixtures/test-collection.json'); + const regularOutputPath = path.join(tmpDir, 'regular-cli-test.html'); + + const command = `node cli.js "${swaggerFile}" "${postmanFile}" --output "${regularOutputPath}"`; + + let stdout; + try { + stdout = execSync(command, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 30000 + }); + } catch (error) { + console.error('Command failed:', error.message); + throw error; + } + + // Verify command output + expect(stdout).toContain('=== Swagger Coverage Report ==='); + expect(stdout).toContain('HTML report saved to:'); + expect(stdout).toContain(regularOutputPath); + + // Verify file was created + expect(fs.existsSync(regularOutputPath)).toBe(true); + + // Verify file content is regular report, not heatmap + const fileContent = fs.readFileSync(regularOutputPath, 'utf8'); + expect(fileContent).toContain('Enhanced Swagger Coverage Report'); + expect(fileContent).not.toContain('API Coverage Heatmap'); + expect(fileContent).not.toContain('API Coverage Matrix'); + expect(fileContent).toContain('Chart.js'); // Regular report uses Chart.js + expect(fileContent).toContain('Coverage Trend Over Time'); + + // Clean up + fs.unlinkSync(regularOutputPath); + }); + + test('CLI should show heatmap option in help', () => { + const command = 'node cli.js --help'; + + const stdout = execSync(command, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8' + }); + + expect(stdout).toContain('--heatmap'); + expect(stdout).toContain('Generate coverage heatmap'); + expect(stdout).toContain('endpoints nodes'); + }); +}); \ No newline at end of file diff --git a/test/heatmap.test.js b/test/heatmap.test.js new file mode 100644 index 0000000..9c82aed --- /dev/null +++ b/test/heatmap.test.js @@ -0,0 +1,178 @@ +const { generateHeatmapReport } = require('../lib/report'); + +describe('Heatmap Report Module', () => { + test('generateHeatmapReport should return HTML string', () => { + const input = { + coverage: 75, + coverageItems: [ + { + method: 'GET', + path: '/users', + name: 'getUsers', + unmatched: false, + matchedRequests: [ + { name: 'Get Users Test' } + ] + }, + { + method: 'POST', + path: '/users', + name: 'createUser', + unmatched: true, + matchedRequests: [] + }, + { + method: 'GET', + path: '/users/{id}', + name: 'getUserById', + unmatched: false, + matchedRequests: [ + { name: 'Get User By ID Test' } + ] + } + ], + meta: { + timestamp: '2023-10-10 10:00:00', + specName: 'Test API', + postmanCollectionName: 'Test Collection', + apiCount: 1, + apiNames: ['Test API'] + } + }; + + const html = generateHeatmapReport(input); + + expect(typeof html).toBe('string'); + expect(html).toContain(' { + const input = { + coverage: 50, + coverageItems: [ + { + method: 'GET', + path: '/users', + name: 'getUsers', + unmatched: false, + matchedRequests: [{ name: 'Test' }] + }, + { + method: 'POST', + path: '/users', + name: 'createUser', + unmatched: true, + matchedRequests: [] + } + ], + meta: { + timestamp: '2023-10-10 10:00:00', + specName: 'Test API', + postmanCollectionName: 'Test Collection' + } + }; + + const html = generateHeatmapReport(input); + + expect(html).toContain('API Coverage Matrix'); + expect(html).toContain('Endpoint Details'); + expect(html).toContain('GET'); + expect(html).toContain('POST'); + expect(html).toContain('/users'); + expect(html).toContain('Covered'); + expect(html).toContain('Not Covered'); + }); + + test('generateHeatmapReport should include statistics', () => { + const input = { + coverage: 33.33, + coverageItems: [ + { + method: 'GET', + path: '/users', + name: 'getUsers', + unmatched: false, + matchedRequests: [{ name: 'Test' }] + }, + { + method: 'POST', + path: '/users', + name: 'createUser', + unmatched: true, + matchedRequests: [] + }, + { + method: 'DELETE', + path: '/users/{id}', + name: 'deleteUser', + unmatched: true, + matchedRequests: [] + } + ], + meta: { + timestamp: '2023-10-10 10:00:00', + specName: 'Test API', + postmanCollectionName: 'Test Collection' + } + }; + + const html = generateHeatmapReport(input); + + // Should show total endpoints + expect(html).toContain('3'); // Total endpoints + expect(html).toContain('1'); // Covered endpoints + expect(html).toContain('2'); // Unique paths (/users and /users/{id}) + expect(html).toContain('33.3%'); // Coverage percentage + }); + + test('generateHeatmapReport should handle empty coverage items', () => { + const input = { + coverage: 0, + coverageItems: [], + meta: { + timestamp: '2023-10-10 10:00:00', + specName: 'Empty API', + postmanCollectionName: 'Empty Collection' + } + }; + + const html = generateHeatmapReport(input); + + expect(html).toContain('API Coverage Heatmap'); + expect(html).toContain('Empty API'); + expect(html).toContain('0'); // Total endpoints + expect(html).toContain('0.0%'); // Coverage + }); + + test('generateHeatmapReport should include interactive features', () => { + const input = { + coverage: 50, + coverageItems: [ + { + method: 'GET', + path: '/test', + name: 'test', + unmatched: false, + matchedRequests: [{ name: 'Test' }] + } + ], + meta: { + timestamp: '2023-10-10 10:00:00', + specName: 'Test API', + postmanCollectionName: 'Test Collection' + } + }; + + const html = generateHeatmapReport(input); + + expect(html).toContain('Toggle Theme'); + expect(html).toContain('Reset View'); + expect(html).toContain('showCellDetails'); + expect(html).toContain('toggleTheme'); + expect(html).toContain('onclick='); + }); +}); \ No newline at end of file