From 9aa9007c374d7876deaf2a21e94e1e54f5a27694 Mon Sep 17 00:00:00 2001 From: kevinccbsg Date: Wed, 30 Sep 2026 15:52:46 +0200 Subject: [PATCH] feat(contracts): print only contract errors and warnings Passing mocks and skipped mocks are counted in the summary line instead of listed one per line. The full detail lives in run.json and index.html. Co-Authored-By: Claude Opus 5.5 --- src/contractReport.js | 17 ++++---------- tests/contractReport.test.js | 45 +++++++++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/contractReport.js b/src/contractReport.js index 818c0fa..d4f2104 100644 --- a/src/contractReport.js +++ b/src/contractReport.js @@ -33,7 +33,11 @@ export function printContractReport(output) { bySource.get(key).push(result); } + // Passing mocks and skips are counted, not listed: run.json and index.html hold the detail. for (const [source, sourceResults] of bySource) { + const noisy = sourceResults.filter((r) => !r.validation.valid || r.validation.warnings.length > 0); + if (noisy.length === 0) continue; + const mode = sourceResults[0]?.mode || 'warn'; const modeLabel = mode === 'error' ? bgRed(` ${mode.toUpperCase()} `) @@ -41,7 +45,7 @@ export function printContractReport(output) { console.log(`${cyan('Source:')} ${source} ${modeLabel}`); console.log(''); - for (const result of sourceResults) { + for (const result of noisy) { const failColor = result.mode === 'error' ? boldRed : boldYellow; const detailColor = result.mode === 'error' ? red : yellow; @@ -55,8 +59,6 @@ export function printContractReport(output) { if (result.mode === 'error') { hasContractErrors = true; } - } else if (result.validation.warnings.length === 0) { - console.log(green(` MOCK ✓ ${result.method} ${result.matchedPath} (${result.status}) — ${formatMockLabel(result)}`)); } for (const warning of result.validation.warnings) { @@ -68,15 +70,6 @@ export function printContractReport(output) { } } - if (skipped.length > 0) { - console.log(dim('Skipped:')); - for (const skip of skipped) { - console.log(dim(` MOCK ℹ "${skip.alias}" — ${skip.url}`)); - console.log(dim(` ${skip.reason === 'urlRegex mock' ? 'Regex URL pattern' : 'No matching path in any spec'}`)); - } - console.log(''); - } - const validatedCount = results.length; if (errorCount === 0 && warningCount === 0) { diff --git a/tests/contractReport.test.js b/tests/contractReport.test.js index 1d98ed4..29cb4ef 100644 --- a/tests/contractReport.test.js +++ b/tests/contractReport.test.js @@ -37,6 +37,38 @@ describe('printContractReport', () => { const logs = stripAnsi(consoleSpy.mock.calls.map(c => c[0]).join('\n')); expect(logs).toContain('All mocks match'); expect(logs).toContain('Mocks validated: 1'); + expect(logs).not.toContain('✓'); + expect(logs).not.toContain('Source:'); + }); + + it('prints a source header only for sources with errors or warnings', () => { + const pass = (specSource) => ({ + alias: 'getPets', method: 'GET', status: 200, specSource, matchedPath: '/v1/pets', mode: 'warn', + validation: { valid: true, errors: [], warnings: [] }, + }); + const output = { + results: [ + pass('./clean.json'), + pass('./noisy.json'), + { + alias: 'createPet', method: 'POST', status: 201, specSource: './noisy.json', matchedPath: '/v1/pets', mode: 'warn', + validation: { + valid: false, + errors: [{ path: 'response.id', message: 'expected integer, got string', keyword: 'type' }], + warnings: [], + }, + }, + ], + skipped: [], + }; + + printContractReport(output); + + const logs = stripAnsi(consoleSpy.mock.calls.map(c => c[0]).join('\n')); + expect(logs).toContain('Source: ./noisy.json'); + expect(logs).not.toContain('./clean.json'); + expect(logs).not.toContain('GET /v1/pets'); + expect(logs).toContain('Mocks validated: 3'); }); it('prints errors with ✗ symbol', () => { @@ -98,7 +130,7 @@ describe('printContractReport', () => { expect(logs).toContain('Warnings: 1'); }); - it('prints skipped mocks with ℹ symbol', () => { + it('counts skipped mocks without listing them', () => { const output = { results: [], skipped: [ @@ -109,8 +141,9 @@ describe('printContractReport', () => { printContractReport(output); const logs = stripAnsi(consoleSpy.mock.calls.map(c => c[0]).join('\n')); - expect(logs).toContain('ℹ'); - expect(logs).toContain('adyenSetup'); + expect(logs).not.toContain('ℹ'); + expect(logs).not.toContain('adyenSetup'); + expect(logs).not.toContain('Skipped:\n'); expect(logs).toContain('Skipped: 1'); }); @@ -198,7 +231,7 @@ describe('printContractReport', () => { mode: 'warn', testName: 'Cart > should load items', occurrence: 1, - validation: { valid: true, errors: [], warnings: [] }, + validation: { valid: true, errors: [], warnings: [{ type: 'UNMATCHED_STATUS', message: 'Status 200 not documented' }] }, }, ], skipped: [], @@ -264,7 +297,7 @@ describe('printContractReport', () => { const lines = consoleSpy.mock.calls.map((c) => stripAnsi(c[0])); const glyphLines = lines.filter((l) => /^\s*(MOCK\s+)?[✓✗⚠ℹ]/.test(l)); - expect(glyphLines.length).toBeGreaterThanOrEqual(4); + expect(glyphLines.length).toBe(2); for (const line of glyphLines) { expect(line).toMatch(/^\s*MOCK [✓✗⚠ℹ]/); } @@ -283,7 +316,7 @@ describe('printContractReport', () => { mode: 'warn', testName: 'Cart > should load items', occurrence: 2, - validation: { valid: true, errors: [], warnings: [] }, + validation: { valid: true, errors: [], warnings: [{ type: 'UNMATCHED_STATUS', message: 'Status 200 not documented' }] }, }, ], skipped: [],